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.