Placeholder Game Data¶
It would be nice to do some queries that actually return some data!
One option would be to fire up a database, define some tables, and load some data in.
… but that would slow us down, and not teach us anything about Lacinia and GraphQL.
Instead, we’ll create an EDN file with some test data in it, and wire that up to the schema. We can fuss with database access and all that later in the tutorial.
cgg-data.edn¶
{:games
[{:id "1234"
:name "Zertz"
:summary "Two player abstract with forced moves and shrinking board"
:minPlayers 2
:maxPlayers 2}
{:id "1235"
:name "Dominion"
:summary "Created the deck-building genre; zillions of expansions"
:minPlayers 2}
{:id "1236"
:name "Tiny Epic Galaxies"
:summary "Fast dice-based sci-fi space game with a bit of chaos"
:minPlayers 1
:maxPlayers 4}
{:id "1237"
:name "7 Wonders: Duel"
:summary "Tense, quick card game of developing civilizations"
:minPlayers 2
:maxPlayers 2}]}
This file defines just a few games I’ve recently played. It will take the place of an external database. Later, we can add more data for the other entities and their relationships.
Although the keys here are camel-case (like Java or JavaScript) and not kebab-case (like Clojure and other Lisps), they are still valid Clojure keywords and, more importantly, they match the field names in the GraphQL schema. Lacinia doesn’t do any trickery here, field names in the schema are matched directly to corresponding keyword keys in the value maps.
Later in this tutorial, we’ll actually connect our application up to an external database.
Resolver¶
Inside our schema
namespace, we need to read the data and provide a resolver
that can access it.
(ns my.clojure-game-geek.schema
"Contains custom resolvers and a function to provide the full schema."
(:require [clojure.java.io :as io]
[com.walmartlabs.lacinia.util :as util]
[com.walmartlabs.lacinia.schema :as schema]
[clojure.edn :as edn]))
(defn resolve-game-by-id
[games-map context args value]
(let [{:keys [id]} args]
(get games-map id)))
(defn resolver-map
[]
(let [cgg-data (-> (io/resource "cgg-data.edn")
slurp
edn/read-string)
games-map (->> cgg-data
:games
(reduce #(assoc %1 (:id %2) %2) {}))]
{:Query/gameById (partial resolve-game-by-id games-map)}))
(defn load-schema
[]
(-> (io/resource "cgg-schema.edn")
slurp
edn/read-string
(util/inject-resolvers (resolver-map))
schema/compile))
You can see a bit of the philosophy of Lacinia inside the load-schema
function: Lacinia strives
to provide only what is most essential, or truly useful and universal.
Lacinia explicitly does not provide a single function to read, parse, inject resolvers, and compile an EDN file in a single call.
That may seem odd – it feels like every application will just cut-and-paste something virtually identical to load-schema
.
In fact, not all schemas will come directly from a single EDN file.
Because the schema is Clojure data it can be constructed, modified, merged, and otherwise transformed
right up to the point that it is compiled.
By starting with a pipeline like the one inside load-schema
, it becomes easy to inject your own application-specific bits
into the steps leading up to schema/compile
, which ultimately becomes quite essential.
Back to the schema; the resolver itself is the resolve-game-by-id
function.
It is provided with a map of games, and the
standard triumvirate of
resolver function arguments: context, field arguments, and container value.
Field resolvers are passed a map of the field arguments (from the client query). This map contains keyword keys, and values of varying types (because field arguments have a type in the GraphQL schema).
We use a bit of destructuring to extract the id [1].
The data in the map is already in a form that matches the GraphQL schema, so it’s
just a matter of get
-ing it out of the games map.
Inside resolver-map
, we read the sample game data, then use typical Clojure data manipulation
to get it into the form that we want: we convert a seq of BoardGame maps into a map of maps, keyed on the :id
of each
BoardGame.
The partial
function is a real workhorse in Clojure code; it takes an existing function and a set of initial arguments
to that function and returns a new function that collects the remaining arguments needed by the original function.
This returned function will accept the standard field resolver arguments – context
, args
, and value
,
and pass the games-map
and those three arguments to resolve-game-by-id
.
This is one common example of the use of higher orderered functions. It’s not as complicated as the term might lead you to believe - just that functions can be arguments to, and return values from, other functions.
Running Queries¶
We’re finally almost ready to run queries … but first, let’s get rid of
that #ordered/map
business.
(ns user
(:require [my.clojure-game-geek.schema :as s]
[com.walmartlabs.lacinia :as lacinia]
[clojure.walk :as walk])
(:import (clojure.lang IPersistentMap)))
(def schema (s/load-schema))
(defn simplify
"Converts all ordered maps nested within the map into standard hash maps, and
sequences into vectors, which makes for easier constants in the tests, and eliminates ordering problems."
[m]
(walk/postwalk
(fn [node]
(cond
(instance? IPersistentMap node)
(into {} node)
(seq? node)
(vec node)
:else
node))
m))
(defn q
[query-string]
(-> (lacinia/execute schema query-string nil nil)
simplify))
This simplify
function finds all the ordered maps and converts them into
ordinary maps.
It also finds any lists and converts them to vectors.
With that in place, we’re ready to reload our code [2], and then run some queries:
(q "{ gameById(id: \"anything\") { id name summary }}")
=> {:data {:gameById nil}}
This hasn’t changed [3], except that, because of simplify
, the final result is just standard maps,
which are easier to look at in the REPL.
However, we can also get real data back from our query:
(q "{ gameById(id: \"1236\") { id name summary minPlayers }}")
=>
{:data {:gameById {:id "1236",
:name "Tiny Epic Galaxies",
:summary "Fast dice-based sci-fi space game with a bit of chaos",
:minPlayers 1}}}
Success! Lacinia has parsed our query string and executed it against our compiled schema. At the correct time, it dropped into our resolver function, which supplied the data that Lacinia then sliced and diced to compose the result map.
You should be able to devise and execute other simple queries at this point.
Summary¶
We’ve extended our schema and field resolvers with test data and are getting some actual data back when we execute a query.
Next up, we’ll continue extending the schema, and start discussing relationships between GraphQL types.
[1] | This is overkill for this very simple case, but it’s nice to demonstrate techniques that are likely to be used in real applications. |
[2] | How to reload your code is going to be specific to your IDE;
Cursive adds a If you are new to Clojure or not using Cursive, this is a big area to dive into; you can start with the Programming at the REPL guide. |
[3] | This REPL output is a bit different than earlier examples; we’ve switched from
the standard clj REPL to the Cursive REPL; the latter pretty-prints
the returned values. |