Browse code
Add Graphql demo
Ed L authored on 30/10/2017 16:00:59
Showing 4 changed files
Showing 4 changed files
- build.sbt
- src/main/resources/graphiql.html
- src/main/scala/com/fiddlerwoaroof/experiments/graphql_addressbook/Hello.scala
- src/test/scala/com/fiddlerwoaroof/experiments/graphql_addressbook/HelloSpec.scala
... | ... |
@@ -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 |
} |