Advanced
In this guide we’ll cover a few advanced topics and show some example code.
Writing Your Queries in .graphql Files
If you don’t like writing your GraphQL queries as part of your ClojureScript code and instead prefer to write them in .graphql
or .gql
files, you can replace your usage of parse-document
with parse-document-files
, which will slurp
your file, so you can do all of the following:
(require '[artemis.document :refer [parse-document-files]])
(parse-document-files "query-a.gql") ; parsing a resource
(parse-document-files "http://my-app.com/query-a.gql") ; parsing a remote file
(parse-document-files (java.io.FileReader. "/Lib/Queries/query-a.gql")) ; parsing a file on the file system
Writing Your Queries in EDN with Venia
If you’re not interested in writing string-style GraphQL queries you may want to give Venia a try. Venia allows you to generate valid GraphQL queries with Clojure data structures.
We can use Venia with Artemis by writing a little macro. Let’s call it vdoc
:
(require '[venia.core :as v])
(defmacro vdoc [source]
(let [s (v/graphql-query source)]
`(artemis.document/parse-document ~s)))
Keep in mind that because the vdoc
macro emits a call to parse-document
, you’ll still have to require the artemis.document
namespace when using it.
Now that we have vdoc
, we can do this:
;; Old string-based query
;; (def planet-info
;; (parse-document
;; "query getBook($id: String!) {
;; book(id: $id) {
;; id
;; title
;; }
;; }"))
;; New venia query
(def planet-info
(vdoc {:venia/operation {:operation/type :query
:operation/name "getBook"}
:venia/variables [{:variable/name "id"
:variable/type :String!}]
:venia/queries [{:query/data [:book
{:id :$id}
[:id :title]]}]}))
Polling
Because Artemis automatically closes our channels at the appropriate time, it’s easy to create a little utility for creating a poll:
(defn poll! [query-fn ms]
(query-fn :remote-only)
(js/setInterval (fn [] (query-fn :local-then-remote)) ms))
Then wrap your query in a function that takes a fetch policy and pass it to poll!
along with a ms
value:
(def planet-doc
(parse-document
"query book($id:ID!) {
book(id:$id) {
title
abstract
}
}"))
(defn q! [fp]
(let [c (a/query! client
book-doc
{:id "cGxhbmV0czox"}
:fetch-policy fp)]
(go-loop []
(when-let [x (<! c)]
(.log js/console x)
(recur)))))
(poll! q! 5000)
GraphQL Subscriptions (experimental)
Most of the guides cover querying and mutating. The GraphQL spec, however, also supports a third operation type: subscriptions. To create subscription with Artemis, call artemis.core/subscribe!
, passing it a client, document, variables, and options. Artemis will attempt to set-up a subscription with your GraphQL server via WebSockets, so it’s essential that your network chain includes a step that establishes the WebSocket connection. For convenience, Artemis includes a basic network step that’ll manage WebSocket-based GraphQL subscriptions:
(require '[artemis.network-steps.ws-subscription :as ws])
(ws/create-ws-subscription-step "ws://localhost:4000/subscriptions")
To combine it with an exisiting step that executes your queries and mutations (the basic http step, for example), you can use the with-ws-subscriptions
helper function.
(require '[artemis.network-steps.http :as http]
'[artemis.network-steps.ws-subscription :as ws])
(def net-chain (ws/with-ws-subscriptions
(http/create-network-step "http://localhost:4000/graphql")
(ws/create-ws-subscription-step "ws://localhost:4000/subscriptions"))
With that all done, you can subscribe:
(let [message-added-doc (parse-document
"subscription($id:ID!) {
messageAdded(channelId:$id) {
id
text
}
}")
message-added-chan (a/subscribe! client message-added-doc {:id 1})]
(go-loop []
(when-let [r (<! message-added-chan)]
(.log js/console "Message added:" r)
(recur))))
Colocating and Composing Queries
Because we’ve been using the parse-document
macro, it’s fairly easy to colocate your GraphQL queries and fragments with your views. parse-document
, however is a macro, and it expects a string as it’s only argument. If you try to do something like (parse-document my-string)
you’ll get a compiler error. There are a few reasons for this.
First, the GraphQL spec strongly encourages static GraphQL queries. By accepting only raw strings, the parse-document
macro ensures that your creating your queries statically.
Second, creating the document AST is a fairly expensive process, so by doing it at compile time we can avoid the runtime overhead.
The macro solution has its benefits, then, but it does pose a problem when you want to dynamically compose multiple queries and fragments together.
To solve this problem, Artemis comes with a artemis.document/compose
function, which takes two or more parsed documents and composes them together to create a single document:
(require '[artemis.core :as a]
'[artemis.document :as d :refer [parse-document]])
(def get-person-query
(parse-document
"query getPerson($id: String!) {
person($id: id) {
id
...PersonFields
}
}"))
(def person-fields-fragment
(parse-document
"fragment PersonFields on Person {
firstName
lastName
avatar {
...ImageFields
}
}"))
(def image-fields-fragment
(parse-document
"fragment ImageFields on Image {
url
size
}"))
(def composed-doc
(d/compose get-person-query
person-fields-fragment
image-fields-fragment))
(a/query! composed-doc {:id 1})
Selecting a Subset of Data
In some cases you might be dealing with a large tree of data, but only need to use a subset of that data. In that case, you can use artemis.document/select
to chose what you want. For example, let’s you’ve have the following data:
{:book
{:id 1,
:title "Book A",
:abstract "Lorem ipsum dolor...",
:author {:id 2, :firstName "Alice", :lastName "Jones"}}}
And, you don’t care about anything but the author’s first and last names. You can use select
to grab exactly what you want:
(require '[artemis.document :as d :refer [parse-document]])
(def doc
(parse-document
"{
book {
author {
firstName
lastName
}
}
}"))
(d/select doc book-data)
;; => {:book {:author {:firstName "Alice", :lastName "Jones"}}}
The previous example used a query to select, but you can also use a fragment:
(def doc
(parse-document
"fragment AuthorNames on Book {
book {
author {
firstName
lastName
}
}
}"))
(d/select doc book-data)
Setting an Auth Header
The default HTTP network step that Artemis comes with support for adding auth headers or cookies. You can do so by simply setting them in context to the network chain. Here are a few examples:
(require '[artemis.core :as a]
'[artemis.network-steps.protocols :refer [GQLNetworkStep]])
(defn basic-auth-step [next-step]
(reify
GQLNetworkStep
(-exec [_ operation context]
(a/exec next-step
operation
(assoc context :basic-auth {:username "bob"
:password "secret"})))))
(defn oauth-step [next-step]
(reify
GQLNetworkStep
(-exec [_ operation context]
(a/exec next-step
operation
(assoc context :basic-auth {:with-credentials? false
:oauth-token "SecretBearerToken"})))))
(defn auth-headers-step [next-step]
(reify
GQLNetworkStep
(-exec [_ operation context]
(a/exec next-step
operation
(assoc context :basic-auth {:with-credentials? false
:headers {"Authorization" "SecretToken"}})))))
You could then use any of the above when setting up your network chain, lets use the last one as an example:
(def client (a/create-client :network-chain (-> (http/create-network-step "/my-graphql-api")
auth-headers-step)))