Lacinia-Pedestal¶
Lacinia is a library for implementing Facebook’s GraphQL specification in idiomatic Clojure.
This library, Lacinia-Pedestal, extends Lacinia to the web, building on the Pedestal framework. It provides Pedestal routes and interceptors to allow GraphQL schemas to be exposed as a web endpoint, as well as providing the GraphIQL client
Overview¶
Warning
The newer com.walmartlabs.lacinia.pedestal2 namespace
is recommended over the
now-deprecated com.walmartlabs.lacinia.pedestal
namespace. The new namespace was
introduced to maintain backwards compatibility, but be aware of which you use,
as the default URLs served differ with each namespace. This guide assumes you
are using the latest pedestal2
namespace and its defaults.
You start with a schema file, resources/hello-world-schema.edn
, in this example:
{:queries
{:hello
{:type String}}}
From there there are three steps:
- Load and compile the schema
- Create a Pedestal service around the schema
- Start the Pedestal service
(ns demo.server
(:require
[clojure.edn :as edn]
[clojure.java.io :as io]
[com.walmartlabs.lacinia.pedestal2 :as p2]
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia.util :as util]
[io.pedestal.http :as http]))
(defn ^:private resolve-hello
[context args value]
"Hello, Clojurians!")
(defn ^:private hello-schema
[]
(-> (io/resource "hello-world-schema.edn")
slurp
edn/read-string
(util/inject-resolvers {:queries/hello resolve-hello})
schema/compile))
(def service (-> (hello-schema)
(p2/default-service nil)
http/create-server
http/start))
At the end of this, an instance of Jetty is launched on port 8888.
The GraphQL endpoint will be at http://localhost:8888/api
and the GraphIQL client will be at
http://localhost:8888/ide
.
The options map provided to default-service
allow a number of features of Lacinia-Pedestal
to be configured or customized, though the intent of default-service
is to just be
initial scaffolding - it should be replaced with application-specific code.
Request Format¶
Clients may send either a HTTP GET or HTTP POST request to execute a query.
In both cases, the request path will (by default) be /api
.
POST application/json¶
When using POST with the application/json
content type, the body of the request may contain the following keys:
query
- Required: The GraphQL query document, as a string.
variables
- Optional: A JSON object of variables (as defined and referenced in the query document).
operationName
- Optional: The name of the specific operation to execute, when the query document defines more than one named operation.
This is the standard and expected request format, and the only one directly supported with the
com.walmartlabs.lacinia.pedestal2
namespace.
GET (deprecated)¶
The GraphQL query document must be provided as query parameter query
.
This is only supported with the com.walmartlabs.lacinia.pedestal
namespace; GET
is not
supported out-of-the-box with pedestal2
.
POST application/graphql (deprecated)¶
Warning
This format is fully supported, but represents a legacy format used internally
at Wal-Mart, prior to the GraphQL community identifying an over-the-wire format.
The POST application/json
format is preferred.
The body of the request should be the GraphQL query document.
If the query document defines variables, they must be specified as the variables
query parameter, as
a string-ified JSON object.
This is only supported with the com.walmartlabs.lacinia.pedestal
namespace; it is not
supported out-of-the-box with pedestal2
.
Response¶
The response to a GraphQL request is a application/json
object.
The response will include a errors
key if there are fatal or
non-fatal errors.
Fatal errors are those that occur before the query is executed; these represents parse failures or validation errors.
Response Format¶
The response format is always application/json
.
The response body is the result map from executing the query (e.g., has data
and/or errors
keys).
In cases where there is a problem parsing or preparing the query, the response will still be in the regular format, e.g.:
{"errors": [{"message": "Request body is empty."}]}
GraphQL supports a third key, extensions
, but does not define what content goes there; it is for application-specific
extensions.
HTTP Status¶
The normal HTTP status is 200 OK
.
If there was a fatal error (such as a query parse error), the HTTP status will be 400 Bad Request
.
Status Conversion¶
Field resolvers may return error maps.
If an error map contains a :status
key, this value will be used
as the overall HTTP status of the response.
When multiple error maps contains :status
, the numerically largest
value is used.
The :status
key is removed from all error maps before the response is streamed to the client.
Async¶
By default, Lacinia-Pedestal blocks the Pedestal request thread
while executing the query. The default
pedestal interceptor stack includes a synchronous query execution handler,
com.walmartlabs.lacinia.pedestal2/query-executor-handler
.
Lacinia also provides an asynchronous query execution handler:
com.walmartlabs.lacinia.pedestal2/async-query-executor-handler
.
When used in the interceptor stack, execution starts on a Pedestal request processing thread, but (at the discretion of individual field resolver functions) may continue on other threads.
Further, the return value from the asychronous handler is a channel, forcing Pedestal to switch to async mode.
Lacinia-Pedestal does not impose any restrictions on the number of requests it will attempt to process concurrently; normally, this is gated by the number of Pedestal request processing threads available.
When using the asynchronous query execution handler, you should provide application-specific interceptors to rate limit requests, or risk saturating your server.
Subscriptions¶
Subscriptions are a way for a client to request notifications about arbitrary events defined by the server; this parallels how a query exposes arbitrary data defined by the server.
The essential support for GraphQL subscriptions is in the main Lacinia library.
Lacinia-Pedestal’s subscription support is designed to be compatible with Apollo GraphQL, a popular library in the JavaScript domain [1]. Like Apollo, Lacinia-Pedestal uses WebSockets to create a durable connection between the client and the server.
Overview¶
A client (typically, a web browser or mobile phone) will establish a connection to the server, and convert it to a full-duplex WebSocket connection.
This single WebSocket connection will be multiplexed to handle any number of subscription requests from the client.
When a subscription is requested, a streamer defined in the GraphQL schema is invoked. A streamer is similar to a field resolver; it has two responsibilities:
- Do whatever setup is necessary, then as new events are available, provide data to a source stream callback function.
- Return a cleanup function that shuts down whatever was previously set up.
Most commonly, a streamer will subscribe to some external feed such as a JMS or Kafka queue, or perhaps a core.async pub or channel.
When a streamer passes nil to the callback, a clean shutdown of the subscription occurs; the client is sent a completion message. The completion message informs the client that the stream of events has completed, and that it should not attempt to reconnect.
The definition of “completed” here is entirely up to the application. For example, a field argument could specify the maximum number of values to stream, and the streamer can pass nil after sufficient values are streamed.
The cleanup function is invoked when the client closes the subscription, when the connection from the client is lost due to a network partition, or when the streamer passes nil to the callback.
Configuration¶
When using com.walmartlabs.lacinia.pedestal2/default-service
, subscriptions are always
enabled, but default-service
is always intended to be replaced in a live application.
The underlying function com.walmartlabs.lacinia.pedestal2/enable-subscriptions
does
the work of enabling subscriptions; the function is passed subscription options:
The following keys are commonly used:
:subscriptions-path
- Path to use in subscriptions WebSocket requests; defaults to
/ws
, :keep-alive-ms
- The interval at which keep-alive messages are sent to the client; defaults to 30 seconds.
:subscription-interceptors
- A seq of interceptors used when processing GraphQL query, mutation, or subscription requests via the WebSocket connection. This is used when overriding the default interceptors.
Further options are described by listener-fn-factory.
Connection Parameters¶
When the client creates a connection, it may pass a payload in the connection_init
message;
this is the connection parameters, and is made available to the streamer and resolver in
the context under the :com.walmartlabs.lacinia/connection-parameters
key.
Endpoint¶
Subscriptions are processed on a second endpoint; normal requests continue to be sent to /api
, but
subscription requests must use /ws
.
The /ws
endpoint does not handle ordinary requests; instead it is used only to establish the
WebSocket connection.
From there, the client sends WebSocket text messages to initiate a subscription, and
the server sends WebSocket text messages for subscription updates and keep alive messages.
Subscription requests are not allowed in /api
path.
GraphiQL¶
GraphiQL, when enabled, is configured with subscriptions enabled; this means that GraphiQL can send subscription
queries.
[1] | Apollo defines a particular contract for how the client and server communicate; this includes heartbeats, and an explicit way for the server to signal to the client that the subscription has completed. The Apollo project also provides clients in several languages. |
Interceptors¶
com.walmartlabs.lacinia.pedestal2 defines Pedestal interceptors and supporting code.
The inject function (added in 0.7.0) adds (or replaces) an interceptor to a vector of interceptors.
Example¶
(ns server
(:require
[com.stuartsierra.component :as component]
[com.walmartlabs.lacinia.pedestal2 :as p2]
[com.walmartlabs.lacinia.pedestal :refer [inject]]
[io.pedestal.interceptor :refer [interceptor]
[io.pedestal.http :as http]))
(defn ^:private extract-user-info
[request]
;; This is very application-specific ...
)
(def ^:private user-info-interceptor
(interceptor
{:name ::user-info
:enter (fn [context]
(let [{:keys [request]} context
user-info (extract-user-info request)]
(assoc-in context [:request :lacinia-app-context :user-info] user-info)))}))
(defn ^:private interceptors
[schema]
(-> (p2/default-interceptors schema nil)
(inject user-info-interceptor :after ::p2/inject-app-context)))
(defn ^:private create-server
[compiled-schema port]
(let [interceptors (interceptors compiled-schema)
routes #{["/api" :post interceptors :route-name ::api]}]
(-> {:env :dev
::http/routes routes
::http/port port
::http/type :jetty
::http/join? false}
http/create-server
http/start)))
(defrecord Server [schema-source server port]
component/Lifecycle
(start [this]
(let [compiled-schema (:schema schema-source)
server' (create-server compiled-schema port)]
(assoc this :server server')))
(stop [this]
(http/stop server)
(assoc this :server nil)))
There’s a lot to process in this more worked example:
- We’re using Component to organize our code and dependencies.
- The schema is provided by a source component (in the next listing), injected as a dependency into the
Server
component. - We’re building our Pedestal service explicitly, rather than using
default-service
.
The interceptor is responsible for putting the user info into the request, and then it’s simple to get that data inside a resolver function:
(ns schema-source
(:require
[com.walmartlabs.lacinia.schema :as schema]
[com.walmartlabs.lacinia.util :as util]
[com.stuartsierra.component :as component]
[clojure.edn :as edn]
[clojure.java.io :as io]))
(defn ^:private resolve-user
[context _args _value]
(let [{:keys [user-info]} context]
;; Use user-info to get the data from somewhere ...
))
(defrecord SchemaSource []
component/Lifecycle
(start [this]
(assoc this :schema (-> (io/resource "schema.edn")
slurp
edn/read-string
(util/inject-resolvers {:queries/user resolve-user})
schema/compile)))
(stop [this]
(dissoc this :schema)))
Again, it’s a little sketchy because we don’t know what the user-info
data is, how its
stored in the request, or what is done with it … but the :user-info
put in place
by the interceptor is a snap to gain access to in any resolver function.
Tip
The inject
function is useful for making one or two small additions to the default interceptors,
but any more than that will likely lead to confusion about what order the items in the interceptor
pipeline are in; better to dupliciate the code from com.walmartlabs.lacinia.pedestal2/default-interceptors
instead.
Request Tracing¶
Lacinia includes support for request tracing, which identifies how much time Lacinia spends parsing, validating, and processing the overall request.
By default, this is enabled by passing the header value lacinia-tracing
set to true
(or any non-blank string).
Further, the default is for the GraphiQL IDE to provide this value; queries using the IDE will trigger tracing behavior (often resulting in very, very large responses).
You will typically want to disable tracing in production, by removing the :com.walmartlabs.lacinia.pedestal2/enable-tracing
interceptor.