add graphql route in s2http
Project: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/commit/dcaa1f34 Tree: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/tree/dcaa1f34 Diff: http://git-wip-us.apache.org/repos/asf/incubator-s2graph/diff/dcaa1f34 Branch: refs/heads/master Commit: dcaa1f34c4dd9bba863d5e532b9b0893e6b9a287 Parents: f41ab56 Author: daewon <dae...@apache.org> Authored: Fri Nov 30 16:46:03 2018 +0900 Committer: daewon <dae...@apache.org> Committed: Fri Nov 30 16:46:03 2018 +0900 ---------------------------------------------------------------------- s2graphql/build.sbt | 12 +- s2graphql/src/main/resources/application.conf | 11 +- s2graphql/src/main/resources/assets/.gitignore | 1 - .../src/main/resources/assets/graphiql.html | 151 ++++++++++++++++++ .../apache/s2graph/graphql/GraphQLServer.scala | 81 ++++------ .../org/apache/s2graph/graphql/HttpServer.scala | 154 ------------------- s2graphql/src/test/resources/application.conf | 11 +- .../apache/s2graph/http/PlayJsonSupport.scala | 4 +- .../s2graph/http/S2GraphMutateRoute.scala | 1 - .../apache/s2graph/http/S2GraphQLRoute.scala | 105 +++++++++++++ .../s2graph/http/SangriaGraphQLSupport.scala | 38 +++++ .../scala/org/apache/s2graph/http/Server.scala | 14 +- 12 files changed, 351 insertions(+), 232 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/build.sbt ---------------------------------------------------------------------- diff --git a/s2graphql/build.sbt b/s2graphql/build.sbt index bc71f98..dbf38c3 100644 --- a/s2graphql/build.sbt +++ b/s2graphql/build.sbt @@ -29,15 +29,11 @@ libraryDependencies ++= Seq( "org.scala-lang" % "scala-compiler" % scalaVersion.value, "org.scala-lang" % "scala-reflect" % scalaVersion.value, - "org.sangria-graphql" %% "sangria" % "1.4.0", - "org.sangria-graphql" %% "sangria-spray-json" % "1.0.0", - "org.sangria-graphql" %% "sangria-play-json" % "1.0.1" % Test, + "org.sangria-graphql" %% "sangria" % "1.4.2", + "org.sangria-graphql" %% "sangria-spray-json" % "1.0.1", + "org.sangria-graphql" %% "sangria-play-json" % "1.0.5" % Test, - "com.typesafe.akka" %% "akka-http" % "10.0.10", - "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.10", - "com.typesafe.akka" %% "akka-slf4j" % "2.4.6", - - "org.scalatest" %% "scalatest" % "3.0.4" % Test + "org.scalatest" %% "scalatest" % "3.0.5" % Test ) Revolver.settings http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/src/main/resources/application.conf ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/resources/application.conf b/s2graphql/src/main/resources/application.conf index aac21d1..b956031 100644 --- a/s2graphql/src/main/resources/application.conf +++ b/s2graphql/src/main/resources/application.conf @@ -16,11 +16,12 @@ # specific language governing permissions and limitations # under the License. # -akka { - loggers = ["akka.event.slf4j.Slf4jLogger"] - event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] - loglevel = "INFO" -} + +//akka { +// loggers = ["akka.event.slf4j.Slf4jLogger"] +// event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] +// loglevel = "INFO" +//} //db.default.url="jdbc:h2:file:./var/metastore;MODE=MYSQL", //db.default.password = sa http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/src/main/resources/assets/.gitignore ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/resources/assets/.gitignore b/s2graphql/src/main/resources/assets/.gitignore deleted file mode 100644 index 8b13789..0000000 --- a/s2graphql/src/main/resources/assets/.gitignore +++ /dev/null @@ -1 +0,0 @@ - http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/src/main/resources/assets/graphiql.html ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/resources/assets/graphiql.html b/s2graphql/src/main/resources/assets/graphiql.html new file mode 100644 index 0000000..3fe0083 --- /dev/null +++ b/s2graphql/src/main/resources/assets/graphiql.html @@ -0,0 +1,151 @@ +<!-- + * LICENSE AGREEMENT For GraphiQL software + * + * Facebook, Inc. (âFacebookâ) owns all right, title and interest, including all + * intellectual property and other proprietary rights, in and to the GraphiQL + * software. Subject to your compliance with these terms, you are hereby granted a + * non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the + * GraphiQL software; and (2) reproduce and distribute the GraphiQL software as + * part of your own software (âYour Softwareâ). Facebook reserves all rights not + * expressly granted to you in this license agreement. + * + * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR + * IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO + * EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICES, DIRECTORS OR EMPLOYEES BE + * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR + * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF + * THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * You will include in Your Software (e.g., in the file(s), documentation or other + * materials accompanying your software): (1) the disclaimer set forth above; (2) + * this sentence; and (3) the following copyright notice: + * + * Copyright (c) 2015, Facebook, Inc. All rights reserved. +--> +<!DOCTYPE html> +<html> + <head> + <style> + body { + height: 100%; + margin: 0; + width: 100%; + overflow: hidden; + } + + #graphiql { + height: 100vh; + } + </style> + + <link rel="stylesheet" href="//cdn.jsdelivr.net/npm/graphiql@0.11.11/graphiql.css" /> + <script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script> + <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script> + <script src="//cdn.jsdelivr.net/react/15.4.2/react.min.js"></script> + <script src="//cdn.jsdelivr.net/react/15.4.2/react-dom.min.js"></script> + <script src="//cdn.jsdelivr.net/npm/graphiql@0.11.11/graphiql.min.js"></script> + </head> + <body> + <div id="graphiql">Loading...</div> + + <script> + + /** + * This GraphiQL example illustrates how to use some of GraphiQL's props + * in order to enable reading and updating the URL parameters, making + * link sharing of queries a little bit easier. + * + * This is only one example of this kind of feature, GraphiQL exposes + * various React params to enable interesting integrations. + */ + + // Parse the search string to get url parameters. + var search = window.location.search; + var parameters = {}; + search.substr(1).split('&').forEach(function (entry) { + var eq = entry.indexOf('='); + if (eq >= 0) { + parameters[decodeURIComponent(entry.slice(0, eq))] = + decodeURIComponent(entry.slice(eq + 1)); + } + }); + + // if variables was provided, try to format it. + if (parameters.variables) { + try { + parameters.variables = + JSON.stringify(JSON.parse(parameters.variables), null, 2); + } catch (e) { + // Do nothing, we want to display the invalid JSON as a string, rather + // than present an error. + } + } + + // When the query and variables string is edited, update the URL bar so + // that it can be easily shared + function onEditQuery(newQuery) { + parameters.query = newQuery; + updateURL(); + } + + function onEditVariables(newVariables) { + parameters.variables = newVariables; + updateURL(); + } + + function onEditOperationName(newOperationName) { + parameters.operationName = newOperationName; + updateURL(); + } + + function updateURL() { + var newSearch = '?' + Object.keys(parameters).filter(function (key) { + return Boolean(parameters[key]); + }).map(function (key) { + return encodeURIComponent(key) + '=' + + encodeURIComponent(parameters[key]); + }).join('&'); + history.replaceState(null, null, newSearch); + } + + // Defines a GraphQL fetcher using the fetch API. + function graphQLFetcher(graphQLParams) { + return fetch('/graphql', { + method: 'post', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(graphQLParams), + credentials: 'include', + }).then(function (response) { + return response.text(); + }).then(function (responseBody) { + try { + return JSON.parse(responseBody); + } catch (error) { + return responseBody; + } + }); + } + + // Render <GraphiQL /> into the body. + ReactDOM.render( + React.createElement(GraphiQL, { + fetcher: graphQLFetcher, + query: parameters.query, + variables: parameters.variables, + operationName: parameters.operationName, + onEditQuery: onEditQuery, + onEditVariables: onEditVariables, + onEditOperationName: onEditOperationName + }), + document.getElementById('graphiql') + ); + </script> + </body> +</html> http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/src/main/scala/org/apache/s2graph/graphql/GraphQLServer.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/org/apache/s2graph/graphql/GraphQLServer.scala b/s2graphql/src/main/scala/org/apache/s2graph/graphql/GraphQLServer.scala index b650714..a013b2d 100644 --- a/s2graphql/src/main/scala/org/apache/s2graph/graphql/GraphQLServer.scala +++ b/s2graphql/src/main/scala/org/apache/s2graph/graphql/GraphQLServer.scala @@ -19,15 +19,9 @@ package org.apache.s2graph.graphql -import java.util.concurrent.Executors - -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import akka.http.scaladsl.model.StatusCodes._ -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server._ import com.typesafe.config.ConfigFactory -import org.apache.s2graph.graphql.middleware.{GraphFormatted, Transform} -import org.apache.s2graph.core.S2Graph +import org.apache.s2graph.graphql.middleware.{GraphFormatted} +import org.apache.s2graph.core.{S2GraphLike} import org.apache.s2graph.core.utils.SafeUpdateCache import org.apache.s2graph.graphql.repository.GraphRepository import org.apache.s2graph.graphql.types.SchemaDef @@ -36,36 +30,42 @@ import sangria.ast.Document import sangria.execution._ import sangria.execution.deferred.DeferredResolver import sangria.marshalling.sprayJson._ -import sangria.parser.{QueryParser, SyntaxError} +import sangria.parser.{SyntaxError} import sangria.schema.Schema import spray.json._ import scala.collection.JavaConverters._ -import scala.concurrent.{ExecutionContext, Future} +import scala.concurrent.{ExecutionContext} import scala.util.control.NonFatal -import scala.util.{Failure, Success, Try} +import scala.util._ -class GraphQLServer() { - val className = Schema.getClass.getName - val logger = LoggerFactory.getLogger(this.getClass) +object GraphQLServer { + def formatError(error: Throwable): JsValue = error match { + case syntaxError: SyntaxError â + JsObject("errors" â JsArray( + JsObject( + "message" â JsString(syntaxError.getMessage), + "locations" â JsArray(JsObject( + "line" â JsNumber(syntaxError.originalError.position.line), + "column" â JsNumber(syntaxError.originalError.position.column)))))) - // Init s2graph - val numOfThread = Runtime.getRuntime.availableProcessors() - val threadPool = Executors.newFixedThreadPool(numOfThread * 2) + case NonFatal(e) â formatError(e.toString) + case e â throw e + } - implicit val ec = ExecutionContext.fromExecutor(threadPool) + def formatError(message: String): JsObject = + JsObject("errors" â JsArray(JsObject("message" â JsString(message)))) +} - val config = ConfigFactory.load() - val s2graph = new S2Graph(config) - val schemaCacheTTL = Try(config.getInt("schemaCacheTTL")).getOrElse(3000) - val enableMutation = Try(config.getBoolean("enableMutation")).getOrElse(false) +class GraphQLServer(s2graph: S2GraphLike, schemaCacheTTL: Int = 60) { + val logger = LoggerFactory.getLogger(this.getClass) val schemaConfig = ConfigFactory.parseMap(Map( SafeUpdateCache.MaxSizeKey -> 1, SafeUpdateCache.TtlKey -> schemaCacheTTL ).asJava) // Manage schema instance lifecycle - val schemaCache = new SafeUpdateCache(schemaConfig) + val schemaCache = new SafeUpdateCache(schemaConfig)(s2graph.ec) def updateEdgeFetcher(requestJSON: spray.json.JsValue)(implicit e: ExecutionContext): Try[Unit] = { val ret = Try { @@ -79,9 +79,9 @@ class GraphQLServer() { ret } - val schemaCacheKey = className + "s2Schema" + val schemaCacheKey = Schema.getClass.getName + "s2Schema" - schemaCache.put(schemaCacheKey, createNewSchema(enableMutation)) + schemaCache.put(schemaCacheKey, createNewSchema(true)) /** * In development mode(schemaCacheTTL = 1), @@ -101,33 +101,17 @@ class GraphQLServer() { newSchema -> s2Repository } - def formatError(error: Throwable): JsValue = error match { - case syntaxError: SyntaxError â - JsObject("errors" â JsArray( - JsObject( - "message" â JsString(syntaxError.getMessage), - "locations" â JsArray(JsObject( - "line" â JsNumber(syntaxError.originalError.position.line), - "column" â JsNumber(syntaxError.originalError.position.column)))))) - - case NonFatal(e) â formatError(e.toString) - case e â throw e - } - - def formatError(message: String): JsObject = - JsObject("errors" â JsArray(JsObject("message" â JsString(message)))) - def onEvictSchema(o: AnyRef): Unit = { logger.info("Schema Evicted") } val TransformMiddleWare = List(org.apache.s2graph.graphql.middleware.Transform()) - def executeGraphQLQuery(query: Document, op: Option[String], vars: JsObject)(implicit e: ExecutionContext) = { + def executeQuery(query: Document, op: Option[String], vars: JsObject)(implicit e: ExecutionContext) = { import GraphRepository._ val (schemaDef, s2Repository) = - schemaCache.withCache(schemaCacheKey, broadcast = false, onEvict = onEvictSchema)(createNewSchema(enableMutation)) + schemaCache.withCache(schemaCacheKey, broadcast = false, onEvict = onEvictSchema)(createNewSchema(true)) val resolver: DeferredResolver[GraphRepository] = DeferredResolver.fetchers(vertexFetcher, edgeFetcher) @@ -142,15 +126,8 @@ class GraphQLServer() { operationName = op, deferredResolver = resolver, middleware = middleWares - ).map((res: spray.json.JsValue) => OK -> res) - .recover { - case error: QueryAnalysisError => - logger.error("Error on execute", error) - BadRequest -> error.resolveError - case error: ErrorWithResolver => - logger.error("Error on execute", error) - InternalServerError -> error.resolveError - } + ) } + } http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/src/main/scala/org/apache/s2graph/graphql/HttpServer.scala ---------------------------------------------------------------------- diff --git a/s2graphql/src/main/scala/org/apache/s2graph/graphql/HttpServer.scala b/s2graphql/src/main/scala/org/apache/s2graph/graphql/HttpServer.scala deleted file mode 100644 index 8b89c73..0000000 --- a/s2graphql/src/main/scala/org/apache/s2graph/graphql/HttpServer.scala +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.apache.s2graph.graphql - -import java.nio.charset.Charset - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._ -import akka.http.scaladsl.model._ -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.{Route, StandardRoute} -import akka.stream.ActorMaterializer -import org.slf4j.LoggerFactory -import sangria.parser.QueryParser -import spray.json._ - -import scala.concurrent.Await -import scala.language.postfixOps -import scala.util._ -import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller, ToResponseMarshallable} -import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} -import akka.util.ByteString -import sangria.ast.Document -import sangria.renderer.{QueryRenderer, QueryRendererConfig} - -import scala.collection.immutable.Seq - -object Server extends App { - val logger = LoggerFactory.getLogger(this.getClass) - - implicit val system = ActorSystem("s2graphql-server") - implicit val materializer = ActorMaterializer() - - import system.dispatcher - import scala.concurrent.duration._ - - import spray.json.DefaultJsonProtocol._ - - val graphQLServer = new GraphQLServer() - - val route: Route = - get { - getFromResource("assets/graphiql.html") - } ~ (post & path("updateEdgeFetcher")) { - entity(as[JsValue]) { body => - graphQLServer.updateEdgeFetcher(body) match { - case Success(_) => complete(StatusCodes.OK -> JsString("Update fetcher finished")) - case Failure(e) => - logger.error("Error on execute", e) - complete(StatusCodes.InternalServerError -> spray.json.JsObject("message" -> JsString(e.toString))) - } - } - } ~ (post & path("graphql")) { - parameters('operationName.?, 'variables.?) { (operationNameParam, variablesParam) => - entity(as[Document]) { document â - variablesParam.map(parseJson) match { - case None â complete(graphQLServer.executeGraphQLQuery(document, operationNameParam, JsObject())) - case Some(Right(js)) â complete(graphQLServer.executeGraphQLQuery(document, operationNameParam, js.asJsObject)) - case Some(Left(e)) â - logger.error("Error on execute", e) - complete(StatusCodes.BadRequest -> graphQLServer.formatError(e)) - } - } ~ entity(as[JsValue]) { body â - val fields = body.asJsObject.fields - - val query = fields.get("query").map(js => js.convertTo[String]) - val operationName = fields.get("operationName").filterNot(_ == JsNull).map(_.convertTo[String]) - val variables = fields.get("variables").filterNot(_ == JsNull) - - query.map(QueryParser.parse(_)) match { - case None â complete(StatusCodes.BadRequest -> graphQLServer.formatError("No query to execute")) - case Some(Failure(error)) â - logger.error("Error on execute", error) - complete(StatusCodes.BadRequest -> graphQLServer.formatError(error)) - case Some(Success(document)) => variables match { - case Some(js) â complete(graphQLServer.executeGraphQLQuery(document, operationName, js.asJsObject)) - case None â complete(graphQLServer.executeGraphQLQuery(document, operationName, JsObject())) - } - } - } - } - } - - val port = sys.props.get("http.port").fold(8000)(_.toInt) - - logger.info(s"Starting GraphQL server... $port") - - Http().bindAndHandle(route, "0.0.0.0", port).foreach { binding => - logger.info(s"GraphQL server ready for connect") - } - - def shutdown(): Unit = { - logger.info("Terminating...") - - system.terminate() - Await.result(system.whenTerminated, 30 seconds) - - logger.info("Terminated.") - } - - // Unmarshaller - - def unmarshallerContentTypes: Seq[ContentTypeRange] = mediaTypes.map(ContentTypeRange.apply) - - def mediaTypes: Seq[MediaType.WithFixedCharset] = - Seq(MediaType.applicationWithFixedCharset("graphql", HttpCharsets.`UTF-8`, "graphql")) - - implicit def documentMarshaller(implicit config: QueryRendererConfig = QueryRenderer.Compact): ToEntityMarshaller[Document] = { - Marshaller.oneOf(mediaTypes: _*) { - mediaType â - Marshaller.withFixedContentType(ContentType(mediaType)) { - json â HttpEntity(mediaType, QueryRenderer.render(json, config)) - } - } - } - - implicit val documentUnmarshaller: FromEntityUnmarshaller[Document] = { - Unmarshaller.byteStringUnmarshaller - .forContentTypes(unmarshallerContentTypes: _*) - .map { - case ByteString.empty â throw Unmarshaller.NoContentException - case data â - import sangria.parser.DeliveryScheme.Throw - QueryParser.parse(data.decodeString(Charset.forName("UTF-8"))) - } - } - - def parseJson(jsStr: String): Either[Throwable, JsValue] = { - val parsed = Try(jsStr.parseJson) - parsed match { - case Success(js) => Right(js) - case Failure(e) => Left(e) - } - } - -} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2graphql/src/test/resources/application.conf ---------------------------------------------------------------------- diff --git a/s2graphql/src/test/resources/application.conf b/s2graphql/src/test/resources/application.conf index 74821e4..31d9cbd 100644 --- a/s2graphql/src/test/resources/application.conf +++ b/s2graphql/src/test/resources/application.conf @@ -16,11 +16,12 @@ # specific language governing permissions and limitations # under the License. # -akka { - loggers = ["akka.event.slf4j.Slf4jLogger"] - event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] - loglevel = "INFO" -} + +//akka { +// loggers = ["akka.event.slf4j.Slf4jLogger"] +// event-handlers = ["akka.event.slf4j.Slf4jEventHandler"] +// loglevel = "INFO" +//} //db.default.url="jdbc:h2:file:./var/metastore;MODE=MYSQL", //db.default.password = sa http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2http/src/main/scala/org/apache/s2graph/http/PlayJsonSupport.scala ---------------------------------------------------------------------- diff --git a/s2http/src/main/scala/org/apache/s2graph/http/PlayJsonSupport.scala b/s2http/src/main/scala/org/apache/s2graph/http/PlayJsonSupport.scala index 244e588..b41ebd8 100644 --- a/s2http/src/main/scala/org/apache/s2graph/http/PlayJsonSupport.scala +++ b/s2http/src/main/scala/org/apache/s2graph/http/PlayJsonSupport.scala @@ -10,10 +10,10 @@ import play.api.libs.json._ trait PlayJsonSupport { - val mediaTypes: Seq[MediaType.WithFixedCharset] = + private val mediaTypes: Seq[MediaType.WithFixedCharset] = Seq(MediaType.applicationWithFixedCharset("json", HttpCharsets.`UTF-8`, "js")) - val unmarshallerContentTypes: Seq[ContentTypeRange] = mediaTypes.map(ContentTypeRange.apply) + private val unmarshallerContentTypes: Seq[ContentTypeRange] = mediaTypes.map(ContentTypeRange.apply) implicit val playJsonMarshaller: ToEntityMarshaller[JsValue] = { Marshaller.oneOf(mediaTypes: _*) { mediaType => http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2http/src/main/scala/org/apache/s2graph/http/S2GraphMutateRoute.scala ---------------------------------------------------------------------- diff --git a/s2http/src/main/scala/org/apache/s2graph/http/S2GraphMutateRoute.scala b/s2http/src/main/scala/org/apache/s2graph/http/S2GraphMutateRoute.scala index a65db5a..e7d1b88 100644 --- a/s2http/src/main/scala/org/apache/s2graph/http/S2GraphMutateRoute.scala +++ b/s2http/src/main/scala/org/apache/s2graph/http/S2GraphMutateRoute.scala @@ -19,7 +19,6 @@ trait S2GraphMutateRoute extends PlayJsonSupport { lazy val parser = new RequestParser(s2graph) - // lazy val requestParser = new RequestParser(s2graph) lazy val exceptionHandler = ExceptionHandler { case ex: JsonParseException => complete(StatusCodes.BadRequest -> ex.getMessage) case ex: java.lang.IllegalArgumentException => complete(StatusCodes.BadRequest -> ex.getMessage) http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2http/src/main/scala/org/apache/s2graph/http/S2GraphQLRoute.scala ---------------------------------------------------------------------- diff --git a/s2http/src/main/scala/org/apache/s2graph/http/S2GraphQLRoute.scala b/s2http/src/main/scala/org/apache/s2graph/http/S2GraphQLRoute.scala new file mode 100644 index 0000000..bf7c92e --- /dev/null +++ b/s2http/src/main/scala/org/apache/s2graph/http/S2GraphQLRoute.scala @@ -0,0 +1,105 @@ +package org.apache.s2graph.http + +import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport +import akka.http.scaladsl.model._ +import akka.http.scaladsl.server.Directives._ +import akka.http.scaladsl.server._ +import org.apache.s2graph.core.S2Graph +import org.apache.s2graph.graphql.GraphQLServer +import org.slf4j.LoggerFactory +import sangria.ast.Document +import sangria.execution.{ErrorWithResolver, QueryAnalysisError} +import sangria.parser.QueryParser +import spray.json.{JsNull, JsObject, JsString, JsValue} + +import scala.util.{Failure, Left, Right, Success, Try} + +object S2GraphQLRoute { + def parseJson(jsStr: String): Either[Throwable, JsValue] = { + import spray.json._ + val parsed = Try(jsStr.parseJson) + + parsed match { + case Success(js) => Right(js) + case Failure(e) => Left(e) + } + } +} + +trait S2GraphQLRoute extends SprayJsonSupport with SangriaGraphQLSupport { + + import S2GraphQLRoute._ + import spray.json.DefaultJsonProtocol._ + import sangria.marshalling.sprayJson._ + + val s2graph: S2Graph + val logger = LoggerFactory.getLogger(this.getClass) + + lazy val graphQLServer = new GraphQLServer(s2graph) + + private val exceptionHandler = ExceptionHandler { + case error: QueryAnalysisError => + logger.error("Error on execute", error) + complete(StatusCodes.BadRequest -> error.resolveError) + case error: ErrorWithResolver => + logger.error("Error on execute", error) + complete(StatusCodes.InternalServerError -> error.resolveError) + } + + lazy val updateEdgeFetcher = path("updateEdgeFetcher") { + entity(as[spray.json.JsValue]) { body => + graphQLServer.updateEdgeFetcher(body)(s2graph.ec) match { + case Success(_) => complete(StatusCodes.OK -> JsString("Update fetcher finished")) + case Failure(e) => + logger.error("Error on execute", e) + complete(StatusCodes.InternalServerError -> spray.json.JsObject("message" -> JsString(e.toString))) + } + } + } + + lazy val graphql = parameters('operationName.?, 'variables.?) { (operationNameParam, variablesParam) => + implicit val ec = s2graph.ec + + entity(as[Document]) { document â + variablesParam.map(parseJson) match { + case None â complete(graphQLServer.executeQuery(document, operationNameParam, JsObject())) + case Some(Right(js)) â complete(graphQLServer.executeQuery(document, operationNameParam, js.asJsObject)) + case Some(Left(e)) â + logger.error("Error on execute", e) + complete(StatusCodes.BadRequest -> GraphQLServer.formatError(e)) + } + } ~ entity(as[spray.json.JsValue]) { body â + val fields = body.asJsObject.fields + + val query = fields.get("query").map(js => js.convertTo[String]) + val operationName = fields.get("operationName").filterNot(_ == JsNull).map(_.convertTo[String]) + val variables = fields.get("variables").filterNot(_ == JsNull) + + query.map(QueryParser.parse(_)) match { + case None â complete(StatusCodes.BadRequest -> GraphQLServer.formatError("No query to execute")) + case Some(Failure(error)) â + logger.error("Error on execute", error) + complete(StatusCodes.BadRequest -> GraphQLServer.formatError(error)) + case Some(Success(document)) => variables match { + case Some(js) â complete(graphQLServer.executeQuery(document, operationName, js.asJsObject)) + case None â complete(graphQLServer.executeQuery(document, operationName, JsObject())) + } + } + } + } + + // expose routes + lazy val graphqlRoute: Route = + get { + getFromResource("assets/graphiql.html") + } ~ + post { + handleExceptions(exceptionHandler) { + concat( + updateEdgeFetcher, + graphql + ) + } + } +} + http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2http/src/main/scala/org/apache/s2graph/http/SangriaGraphQLSupport.scala ---------------------------------------------------------------------- diff --git a/s2http/src/main/scala/org/apache/s2graph/http/SangriaGraphQLSupport.scala b/s2http/src/main/scala/org/apache/s2graph/http/SangriaGraphQLSupport.scala new file mode 100644 index 0000000..965e17a --- /dev/null +++ b/s2http/src/main/scala/org/apache/s2graph/http/SangriaGraphQLSupport.scala @@ -0,0 +1,38 @@ +package org.apache.s2graph.http + +import java.nio.charset.Charset + +import akka.http.scaladsl.marshalling.{Marshaller, ToEntityMarshaller} +import akka.http.scaladsl.model._ +import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, Unmarshaller} +import akka.util.ByteString +import sangria.ast.Document +import sangria.parser.QueryParser +import sangria.renderer.{QueryRenderer, QueryRendererConfig} + +trait SangriaGraphQLSupport { + private val mediaTypes: Seq[MediaType.WithFixedCharset] = + Seq(MediaType.applicationWithFixedCharset("graphql", HttpCharsets.`UTF-8`, "graphql")) + + private val unmarshallerContentTypes: Seq[ContentTypeRange] = mediaTypes.map(ContentTypeRange.apply) + + implicit def documentMarshaller(implicit config: QueryRendererConfig = QueryRenderer.Compact): ToEntityMarshaller[Document] = { + Marshaller.oneOf(mediaTypes: _*) { + mediaType â + Marshaller.withFixedContentType(ContentType(mediaType)) { + json â HttpEntity(mediaType, QueryRenderer.render(json, config)) + } + } + } + + implicit val documentUnmarshaller: FromEntityUnmarshaller[Document] = { + Unmarshaller.byteStringUnmarshaller + .forContentTypes(unmarshallerContentTypes: _*) + .map { + case ByteString.empty â throw Unmarshaller.NoContentException + case data â + import sangria.parser.DeliveryScheme.Throw + QueryParser.parse(data.decodeString(Charset.forName("UTF-8"))) + } + } +} http://git-wip-us.apache.org/repos/asf/incubator-s2graph/blob/dcaa1f34/s2http/src/main/scala/org/apache/s2graph/http/Server.scala ---------------------------------------------------------------------- diff --git a/s2http/src/main/scala/org/apache/s2graph/http/Server.scala b/s2http/src/main/scala/org/apache/s2graph/http/Server.scala index c26e314..00146f6 100644 --- a/s2http/src/main/scala/org/apache/s2graph/http/Server.scala +++ b/s2http/src/main/scala/org/apache/s2graph/http/Server.scala @@ -36,7 +36,8 @@ import org.slf4j.LoggerFactory object Server extends App with S2GraphTraversalRoute with S2GraphAdminRoute - with S2GraphMutateRoute { + with S2GraphMutateRoute + with S2GraphQLRoute { implicit val system: ActorSystem = ActorSystem("S2GraphHttpServer") implicit val materializer: ActorMaterializer = ActorMaterializer() @@ -57,15 +58,20 @@ object Server extends App pathPrefix("graphs")(traversalRoute), pathPrefix("mutate")(mutateRoute), pathPrefix("admin")(adminRoute), + pathPrefix("graphql")(graphqlRoute), get(complete(health)) ) val binding: Future[Http.ServerBinding] = Http().bindAndHandle(routes, "localhost", port) binding.onComplete { case Success(bound) => logger.info(s"Server online at http://${bound.localAddress.getHostString}:${bound.localAddress.getPort}/") - case Failure(e) => - logger.error(s"Server could not start!", e) - system.terminate() + case Failure(e) => logger.error(s"Server could not start!", e) + } + + scala.sys.addShutdownHook { () => + s2graph.shutdown() + system.terminate() + logger.info("System terminated") } Await.result(system.whenTerminated, Duration.Inf)