git.fiddlerwoaroof.com
Browse code

Add Graphql demo

Ed L authored on 30/10/2017 16:00:59
Showing 5 changed files
1 1
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+target
... ...
@@ -4,13 +4,26 @@ version      := "0.1.0-SNAPSHOT"
4 4
 
5 5
 name         := "graphql-addressbook"
6 6
 
7
+scalacOptions += "-Ypartial-unification"
8
+
7 9
 libraryDependencies +=  "org.scalatest" %% "scalatest" % "3.0.3"
8 10
 
9
-libraryDependencies += "org.sangria-graphql" %% "sangria" % "1.3.1"
10
-libraryDependencies += "org.sangria-graphql" %% "sangria-circe" % "1.1.0"
11
+libraryDependencies ++= Seq(
12
+  ("sangria",            "1.3.1"),
13
+  ("sangria-circe",      "1.1.0"),
14
+  ("sangria-spray-json", "1.0.0")
15
+).map({case (artifactStr,versionStr) => "org.sangria-graphql" %% artifactStr % versionStr})
11 16
 
12 17
 libraryDependencies ++= Seq(
13 18
   "io.circe" %% "circe-core",
14 19
   "io.circe" %% "circe-generic",
15 20
   "io.circe" %% "circe-parser"
16 21
 ).map(_ % "0.8.0")
22
+
23
+libraryDependencies ++= Seq(
24
+  "com.typesafe.akka" %% "akka-http" % "10.0.10",
25
+  "com.typesafe.akka" %% "akka-http-spray-json" % "10.0.10"
26
+)
27
+
28
+libraryDependencies += "org.typelevel" %% "cats-core" % "1.0.0-MF"
29
+
17 30
new file mode 100644
... ...
@@ -0,0 +1,151 @@
1
+<!--
2
+ * LICENSE AGREEMENT For GraphiQL software
3
+ *
4
+ * Facebook, Inc. (“Facebook”) owns all right, title and interest, including all
5
+ * intellectual property and other proprietary rights, in and to the GraphiQL
6
+ * software. Subject to your compliance with these terms, you are hereby granted a
7
+ * non-exclusive, worldwide, royalty-free copyright license to (1) use and copy the
8
+ * GraphiQL software; and (2) reproduce and distribute the GraphiQL software as
9
+ * part of your own software (“Your Software”). Facebook reserves all rights not
10
+ * expressly granted to you in this license agreement.
11
+ *
12
+ * THE SOFTWARE AND DOCUMENTATION, IF ANY, ARE PROVIDED "AS IS" AND ANY EXPRESS OR
13
+ * IMPLIED WARRANTIES (INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
14
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE) ARE DISCLAIMED. IN NO
15
+ * EVENT SHALL FACEBOOK OR ITS AFFILIATES, OFFICES, DIRECTORS OR EMPLOYEES BE
16
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
18
+ * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
19
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
20
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
21
+ * THE USE OF THE SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
22
+ *
23
+ * You will include in Your Software (e.g., in the file(s), documentation or other
24
+ * materials accompanying your software): (1) the disclaimer set forth above; (2)
25
+ * this sentence; and (3) the following copyright notice:
26
+ *
27
+ * Copyright (c) 2015, Facebook, Inc. All rights reserved.
28
+-->
29
+<!DOCTYPE html>
30
+<html>
31
+<head>
32
+    <style>
33
+        body {
34
+            height: 100%;
35
+            margin: 0;
36
+            width: 100%;
37
+            overflow: hidden;
38
+        }
39
+
40
+        #graphiql {
41
+            height: 100vh;
42
+        }
43
+    </style>
44
+
45
+    <link rel="stylesheet" href="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.css" />
46
+    <script src="//cdn.jsdelivr.net/es6-promise/4.0.5/es6-promise.auto.min.js"></script>
47
+    <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
48
+    <script src="//cdn.jsdelivr.net/react/15.3.2/react.min.js"></script>
49
+    <script src="//cdn.jsdelivr.net/react/15.3.2/react-dom.min.js"></script>
50
+    <script src="//cdn.jsdelivr.net/graphiql/0.8.0/graphiql.min.js"></script>
51
+</head>
52
+<body>
53
+<div id="graphiql">Loading...</div>
54
+
55
+<script>
56
+
57
+    /**
58
+     * This GraphiQL example illustrates how to use some of GraphiQL's props
59
+     * in order to enable reading and updating the URL parameters, making
60
+     * link sharing of queries a little bit easier.
61
+     *
62
+     * This is only one example of this kind of feature, GraphiQL exposes
63
+     * various React params to enable interesting integrations.
64
+     */
65
+
66
+        // Parse the search string to get url parameters.
67
+    var search = window.location.search;
68
+    var parameters = {};
69
+    search.substr(1).split('&').forEach(function (entry) {
70
+        var eq = entry.indexOf('=');
71
+        if (eq >= 0) {
72
+            parameters[decodeURIComponent(entry.slice(0, eq))] =
73
+                decodeURIComponent(entry.slice(eq + 1));
74
+        }
75
+    });
76
+
77
+    // if variables was provided, try to format it.
78
+    if (parameters.variables) {
79
+        try {
80
+            parameters.variables =
81
+                JSON.stringify(JSON.parse(parameters.variables), null, 2);
82
+        } catch (e) {
83
+            // Do nothing, we want to display the invalid JSON as a string, rather
84
+            // than present an error.
85
+        }
86
+    }
87
+
88
+    // When the query and variables string is edited, update the URL bar so
89
+    // that it can be easily shared
90
+    function onEditQuery(newQuery) {
91
+        parameters.query = newQuery;
92
+        updateURL();
93
+    }
94
+
95
+    function onEditVariables(newVariables) {
96
+        parameters.variables = newVariables;
97
+        updateURL();
98
+    }
99
+
100
+    function onEditOperationName(newOperationName) {
101
+        parameters.operationName = newOperationName;
102
+        updateURL();
103
+    }
104
+
105
+    function updateURL() {
106
+        var newSearch = '?' + Object.keys(parameters).filter(function (key) {
107
+            return Boolean(parameters[key]);
108
+        }).map(function (key) {
109
+            return encodeURIComponent(key) + '=' +
110
+                encodeURIComponent(parameters[key]);
111
+        }).join('&');
112
+        history.replaceState(null, null, newSearch);
113
+    }
114
+
115
+    // Defines a GraphQL fetcher using the fetch API.
116
+    function graphQLFetcher(graphQLParams) {
117
+        return fetch('/graphql', {
118
+            method: 'post',
119
+            headers: {
120
+                'Accept': 'application/json',
121
+                'Content-Type': 'application/json',
122
+            },
123
+            body: JSON.stringify(graphQLParams),
124
+            credentials: 'include',
125
+        }).then(function (response) {
126
+            return response.text();
127
+        }).then(function (responseBody) {
128
+            try {
129
+                return JSON.parse(responseBody);
130
+            } catch (error) {
131
+                return responseBody;
132
+            }
133
+        });
134
+    }
135
+
136
+    // Render <GraphiQL /> into the body.
137
+    ReactDOM.render(
138
+        React.createElement(GraphiQL, {
139
+            fetcher: graphQLFetcher,
140
+            query: parameters.query,
141
+            variables: parameters.variables,
142
+            operationName: parameters.operationName,
143
+            onEditQuery: onEditQuery,
144
+            onEditVariables: onEditVariables,
145
+            onEditOperationName: onEditOperationName
146
+        }),
147
+        document.getElementById('graphiql')
148
+    );
149
+</script>
150
+</body>
151
+</html>
0 152
\ No newline at end of file
... ...
@@ -1,21 +1,91 @@
1 1
 package com.fiddlerwoaroof.experiments.graphql_addressbook
2 2
 
3
+import akka.actor.ActorSystem
4
+import akka.http.scaladsl.Http
5
+import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
6
+import akka.http.scaladsl.model.StatusCodes._
7
+import akka.http.scaladsl.server.Directives._
8
+import akka.http.scaladsl.server._
9
+import akka.stream.ActorMaterializer
10
+import sangria.ast.Document
11
+import sangria.execution.{ErrorWithResolver, Executor, QueryAnalysisError}
3 12
 import sangria.macros.derive._
4
-import sangria.macros._
13
+import sangria.marshalling.sprayJson._
14
+import sangria.parser.QueryParser
5 15
 import sangria.schema._
16
+import spray.json.{JsObject, JsString, JsValue}
6 17
 
7
-import sangria.execution._
8
-import sangria.marshalling.circe._
18
+import cats.Monoid
19
+import cats.instances.all._
9 20
 
10
-import io.circe.Json
21
+
22
+import scala.util.{Failure, Success}
11 23
 
12 24
 trait Identifiable {
13 25
   def id: String
14 26
 }
15 27
 
28
+class AddressBook {
29
+
30
+  import AddressBook.PNHelper
31
+
32
+  private val Contacts = List(
33
+    Contact("1",
34
+      EnglishName("John", Some("Apple"), "Seed"),
35
+      Map("home" -> Address("1103 Foo St.", "Ventura", "CA", "93003", "USA")),
36
+      Map("home" -> pn"333-444-3333")),
37
+    Contact("1",
38
+      EnglishName("Bob", None, "Marley"),
39
+      Map("home" -> Address("1103 Maricopa Ave.", "Ventura", "CA", "93003", "USA")),
40
+      Map("home" -> pn"435-2039")),
41
+  )
42
+
43
+  def contact(id: String): Option[Contact] =
44
+    Contacts find (_.id == id)
45
+
46
+  def addressesByPartialName(name: String, addressType: String = "home"): Seq[Address] =
47
+    Contacts
48
+      .filter(_.name.name.toLowerCase contains name)
49
+      .flatMap(_.addresses get addressType)
50
+
51
+  def addresses: List[Contact] = Contacts
52
+}
53
+
54
+object AddressBook {
55
+
56
+  implicit class PNHelper(private val sc: StringContext) extends AnyVal {
57
+    def pn(args: Any*): PhoneNumber = {
58
+      val str = sc.parts.head
59
+      val parts = str.split('-')
60
+      parts match {
61
+        case Array(cc, ac, pref, suf) => PhoneNumber(cc.toInt, ac.toInt, pref.toInt, suf.toInt)
62
+        case Array(ac, pref, suf) => PhoneNumber(1, ac.toInt, pref.toInt, suf.toInt)
63
+        case Array(pref, suf) => PhoneNumber(1, 805, pref.toInt, suf.toInt)
64
+      }
65
+    }
66
+  }
67
+
68
+}
69
+
16 70
 case class Picture(width: Int, height: Int, url: Option[String])
17 71
 
18
-case class Product(id: String, name: String, description: String) extends Identifiable {
72
+case class PhoneNumber(countryCode: Int, areaCode: Int, prefix: Int, suffix: Int)
73
+
74
+case class Address(address: String, city: String, state: String, zip: String, country: String)
75
+
76
+trait Name {
77
+  def name: String
78
+
79
+  def sortName: String
80
+}
81
+
82
+case class EnglishName(first: String, middle: Option[String], last: String) extends Name {
83
+  def name: String = s"$first ${middle.map(x => s"$x ").getOrElse("")}$last"
84
+
85
+  def sortName: String = s"$last, $first"
86
+}
87
+
88
+case class Contact(id: String, name: Name, addresses: Map[String, Address], phoneNumbers: Map[String, PhoneNumber]) extends Identifiable {
19 89
   def picture(size: Int): Picture =
20 90
     Picture(
21 91
       width = size,
... ...
@@ -23,7 +93,7 @@ case class Product(id: String, name: String, description: String) extends Identi
23 93
       url = Some(s"//cdn.com/$size/$id.jpg"))
24 94
 }
25 95
 
26
-object Hello extends Greeting with App {
96
+object Hello extends App {
27 97
   implicit val PictureType =
28 98
     deriveObjectType[Unit, Picture](
29 99
       ObjectTypeDescription("The product picture"),
... ...
@@ -36,55 +106,78 @@ object Hello extends Greeting with App {
36 106
       fields[Unit, Identifiable](
37 107
         Field("id", StringType, resolve = _.value.id)))
38 108
 
39
-  val ProductType =
40
-    deriveObjectType[Unit, Product](
109
+  val AddressType =
110
+    deriveObjectType[Unit, Address](
41 111
       Interfaces(IdentifiableType),
42
-      IncludeMethods("picture"))
43
-
44
-  class ProductRepo {
45
-    private val Products = List(
46
-      Product("1", "Cheescake", "Tasty"),
47
-      Product("2", "HEalth Potion", "+50 HP"),
48
-    )
49
-
50
-    def product(id: String): Option[Product] =
51
-      Products find (_.id == id)
52
-
53
-    def products: List[Product] = Products
54
-  }
112
+      IncludeMethods("picture", "sortName", "name"))
55 113
 
56 114
   val Id = Argument("id", StringType)
57 115
 
58 116
   val QueryType =
59
-    ObjectType("Query", fields[ProductRepo, Unit](
60
-      Field("product", OptionType(ProductType),
117
+    ObjectType("Query", fields[AddressBook, Unit](
118
+      Field("contact", OptionType(ContactType),
61 119
         description = Some("Return product with specific `id`."),
62 120
         arguments = Id :: Nil,
63
-        resolve = c => c.ctx.product(c arg Id)),
121
+        resolve = c => c.ctx.contact(c arg Id)),
64 122
 
65
-      Field("products", ListType(ProductType),
123
+      Field("addressByPartialName", ListType(AddressType),
124
+        description = Some("Return product with specific `id`."),
125
+        arguments = Id :: Nil,
126
+        resolve = c => c.ctx.addressByPartialName(c arg Id)),
127
+
128
+      Field("addresses", ListType(AddressType),
66 129
         description = Some("Returns all products"),
67
-        resolve = _.ctx.products)
130
+        resolve = _.ctx.addresses)
68 131
     ))
69 132
 
70
-  val query = graphql"""
71
-      query MyProduct {
72
-        product(id: "2") {
73
-          name
74
-          description
75
-
76
-          picture(size: 500) {
77
-            width, height, url
78
-          }
79
-        }
80
-
81
-        products {
82
-          name
83
-        }
84
-      }"""
85
-}
133
+  val schema = Schema(QueryType)
134
+
135
+  implicit val system = ActorSystem("sangria-server")
136
+  implicit val materializer = ActorMaterializer()
137
+
138
+  import system.dispatcher
139
+
140
+  val route: Route =
141
+    (post & path("graphql")) {
142
+      entity(as[JsValue]) {
143
+        requestJson => graphQLEndpoint(requestJson)
144
+      }
145
+    } ~
146
+      get {
147
+        getFromResource("graphiql.html")
148
+      }
149
+
150
+  def graphQLEndpoint(requestJson: JsValue) = {
151
+    val JsObject(fields) = requestJson
152
+    val JsString(query) = fields("query")
153
+
154
+    val operation = fields.get("operationName") collect {
155
+      case JsString(op) => op
156
+    }
157
+
158
+    val vars = fields.get("variables") match {
159
+      case Some(obj: JsObject) => obj
160
+      case _ => JsObject.empty
161
+    }
162
+
163
+    QueryParser.parse(query) match {
164
+      case Success(queryAst) =>
165
+        complete(executeGraphQLQuery(queryAst, operation, vars))
166
+      case Failure(error) =>
167
+        complete(BadRequest, JsObject("error" -> JsString(error.getMessage)))
168
+    }
169
+  }
86 170
 
87
-trait Greeting {
88
-  lazy val greeting: String = "hello"
171
+  def executeGraphQLQuery(query: Document, op: Option[String], vars: JsObject) =
172
+    Executor.execute(schema, query, new AddressBook,
173
+      variables = vars,
174
+      operationName = op)
175
+      .map(OK -> _)
176
+      .recover {
177
+        case error: QueryAnalysisError => BadRequest -> error.resolveError
178
+        case error: ErrorWithResolver => InternalServerError -> error.resolveError
179
+      }
180
+
181
+  Http().bindAndHandle(route, "0.0.0.0", 4930)
89 182
 }
90 183
 
... ...
@@ -3,7 +3,7 @@ package com.fiddlerwoaroof.experiments.graphql_addressbook
3 3
 import org.scalatest._
4 4
 
5 5
 class HelloSpec extends FlatSpec with Matchers {
6
-  "The Hello object" should "say hello" in {
7
-    Hello.greeting shouldEqual "hello"
8
-  }
6
+//  "The Hello object" should "say hello" in {
7
+//    Hello.greeting shouldEqual "hello"
8
+//  }
9 9
 }