/lacinia

GraphQL implementation in pure Clojure

Primary LanguageClojureOtherNOASSERTION

Lacinia

Clojars Project

CircleCI

Lacinia Manual

API Documentation

This library is a full implementation of the GraphQL specification and aims to maintain external1 compliance with the specification.

It should be viewed as roughly analogous to the official reference JavaScript implementation. In other words, it is a backend-agnostic GraphQL query execution engine.

It provides:

  • A pure data schema definition DSL. Define the GraphQL schema your server exposes using data (a simple EDN file will do). Process and augment your schema with ordinary functions prior to handing it to this library. Add entry-points (known as resolvers) to populate an executed data structure based on the query.
  • A query parser. Given a compliant GraphQL query, yield a Clojure data structure.
  • A query validator.
  • A built-in query execution engine, with asynchronous execution support. Given a query and a schema, traverse the query and return a data structure. This data structure will typically be serialized to JSON and returned.

Core philosophies:

  • Data, not macros. Schemas are data and you can manipulate them as such.
  • It's impossible for this core library to make assumptions about every backend it might be running on. Define your own execution path to optimize backend queries and leverage the underlying infrastructure (query parsing, validation, schema, and more).
  • Webserver agnostic. You can use use this with any Clojure web stack (or not with a webserver at all).
  • No magic. Use this for full query execution lifecycle or use the portions you want.
  • Embrace clojure.spec internally and externally. For instance, custom scalar types are expected to be defined as conformers.

Getting Started

For more detailed documentation, read the manual.

GraphQL starts with a schema definition of exposed types.

A schema starts as an EDN file; the example below demonstrates several of the available options:

{:enums
 {:episode
  {:description "The episodes of the original Star Wars trilogy."
   :values [:NEWHOPE :EMPIRE :JEDI]}}

 :objects
 {:droid
  {:fields {:primary_functions {:type (list String)}
            :id {:type Int}
            :name {:type String}
            :appears_in {:type (list :episode)}}}

  :human
  {:fields {:id {:type Int}
            :name {:type String}
            :home_planet {:type String}
            :appears_in {:type (list :episode)}}}}

 :queries
 {:hero {:type (non-null :human)
         :args {:episode {:type :episode}}
         :resolve :get-hero}
  :droid {:type :droid
          :args {:id {:type String :default-value "2001"}}
          :resolve :get-droid}}}

A schema alone describes what data is available to clients, but doesn't identify where the data comes from; that's the job of a field resolver, provided by the :resolve key inside files such as the :hero and :droid query.

The values here, :get-hero and :get-droid, are placeholders; the startup code of the application will use com.walmartlabs.lacinia.util/attach-resolvers to attach the actual field resolver function.

A field resolver is just a function which is passed the application context, a map of arguments to values, and a resolved value from a parent field. The field resolver returns a value. If it's a scalar type, it should return a value that conforms to the defined type in the schema. If not, it's a type error.

The field resolver is totally responsible for obtaining the data from whatever external store you use: whether it is a database, a web service, or something else.

It's important to understand that every field has a field resolver, even if you don't define it. If you don't define one, Lacinia provides a default field resolver.

Here's what the get-hero field resolver might look like:

(defn get-hero [context arguments value]
  (let [{:keys [episode]} arguments]
    (if (= episode :NEWHOPE)
      {:id 1000
       :name "Luke"
       :home-planet "Tatooine"
       :appears-in ["NEWHOPE" "EMPIRE" "JEDI"]}
      {:id 2000
       :name "Lando Calrissian"
       :home-planet "Socorro"
       :appears-in ["EMPIRE" "JEDI"]})))

The field resolver can simply return the resolved value. Field resolvers that return multiple values return a seq of values.

After attaching resolvers, it is necessary to compile the schema; this step performs validations, provide defaults, and organizes the schema for efficient execution of queries.

This needs only be done once, in application startup code:

(require '[clojure.edn :as edn]
         '[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
         '[com.walmartlabs.lacinia.schema :as schema])

(def star-wars-schema
  (-> "schema.edn"
      slurp
      edn/read-string
      (attach-resolvers {:get-hero get-hero
                         :get-droid (constantly {})})
      schema/compile))

With the compiled application available, it can be used to execute requests; this typically occurs inside a Ring handler function:

(require '[com.walmartlabs.lacinia :refer [execute]]
         '[clojure.data.json :as json])

(defn handler [request]
  {:status 200
   :headers {"Content-Type" "application/json"}
   :body (let [query (get-in request [:query-params :query])]
           (->> {:request request}
                (execute star-wars-schema query nil)
                json/write-str))})

Lacinia doesn't know about the web tier at all, it just knows about parsing and executing queries against a compiled schema. A companion library, pedestal-lacinia, is one way to expose your schema on the web.

User queries are provided as the body of a request with the content type application/graphql. It looks a lot like JSON.

{
  hero {
    id
    name
  }
}

The execute function returns EDN data that can be easily converted to JSON. The :data key contains the value requested for the hero query in the request.

{:data
  {:hero {:id 2000
          :name "Lando Calrissian"}}}

This example request has no errors, and contained only a single query. GraphQL supports multiple queries in a single request. There may be errors executing the query, Lacinia will process as much as it can, and will report errors in the :errors key.

One of the benefits of GraphQL is that the client has the power to rename fields in the response:

{
  hero(episode: NEWHOPE) {
    movies: appears_in
  }
}
{:data {:hero {:movies [:NEWHOPE :EMPIRE :JEDI]}}}

Status

Although this library is used in production at Walmart, it is still considered alpha software - subject to change. We expect to stabilize it in the near future.

To use this library with Clojure 1.9-alpha14, specify an exclusion like: [com.walmartlabs/lacinia "x.x.x" :exclusions [clojure-future-spec]].

More details are in the manual.

License

Copyright © 2017 WalmartLabs

Distributed under the Apache License, Version 2.0.

Footnotes

[1] External compliance means that the edges should perform the same as another GraphQL library, but the internal algorithms to achieve that result may be different and deviate from specification in order to work in a functional way.