In this guide we'll cover a few advanced topics and show some example code.
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
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]]}]}))
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)
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))))
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})
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)
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)))
Can you improve this documentation?Edit on GitHub
cljdoc is a website building & hosting documentation for Clojure/Script libraries
× close