add graphql route in s2http
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 @@
"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
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
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 @@
-
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>
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.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
+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))))))
+
+ case NonFatal(e) ⇒ formatError(e.toString)
+ case e ⇒ throw e
+ }
+
+ def formatError(message: String): JsObject =
+ JsObject("errors" → JsArray(JsObject("message" → JsString(message))))
+}
+
+class GraphQLServer(s2graph: S2GraphLike, schemaCacheTTL: Int = 60) {
val logger = LoggerFactory.getLogger(this.getClass)
- // Init s2graph
- val numOfThread = Runtime.getRuntime.availableProcessors()
- val threadPool = Executors.newFixedThreadPool(numOfThread * 2)
-
- implicit val ec = ExecutionContext.fromExecutor(threadPool)
-
- 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)
-
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 @@
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 @@
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 @@
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
- }
+ )
}
+
}
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)
- }
- }
-
-}
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
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 @@
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 =>
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 @@
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)
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
+ )
+ }
+ }
+}
+
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")))
+ }
+ }
+}
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 @@
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 @@
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)