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

dev-resources/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.

src/my/clojure_game_geek/schema.clj
(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.

dev-resources/user.clj
(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 Load File in REPL action that loads the current file and any changed namespaces in dependency order automatically.

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.