NOTE: this whole thing so far has been entirely ai-generated (except for this paragraph). I need to make a pass over it before it's ready to be officially released.
A lightweight alternative to pathom3 for Clojure.
This library provides a minimal resolver-based data fetching engine that supports:
- Simple resolvers with declared inputs and outputs
- Nested queries (EQL-style joins)
- Nested inputs (resolvers that require sub-attributes of their inputs)
- Optional inputs (
[:? :key]syntax) - Optional query items (
[:? :key]in query vectors) - Global resolvers (no input required)
- Var-based resolvers (metadata-driven, REPL-friendly)
- Batch resolvers (process multiple entities at once, breadth-first)
- Transitive resolution — automatically chains resolvers to satisfy dependencies
- Strict mode — throws when data can't be resolved
- Plugin system
- Lenient mode
- Query planning (uses the input query directly)
- EQL AST manipulation
Add to your deps.edn:
{:deps {com.biffweb/graph {:git/url "https://github.com/jacobobryant/biff.graph"
:git/sha "..."}}}Define resolvers as regular functions with metadata:
(require '[com.biffweb.graph :as biff.graph])
(defn user-by-id
{:input [:user/id]
:output [:user/name :user/email]}
[ctx {:user/keys [id]}]
;; fetch from db, etc.
{:user/name "Alice" :user/email "alice@example.com"})
(defn user-friends
{:input [:user/id]
:output [:user/friends]}
[ctx {:user/keys [id]}]
{:user/friends [{:user/id 2} {:user/id 3}]})(def index (biff.graph/build-index [#'user-by-id #'user-friends]))(biff.graph/query {:biff.graph/index index}
{:user/id 1}
[:user/name {:user/friends [:user/name]}])
;; => {:user/name "Alice"
;; :user/friends [{:user/name "Bob"} {:user/name "Carol"}]}Use [:? :key] to mark inputs as optional. When the optional input can't be resolved,
it is simply omitted from the input map passed to the resolver:
(defn user-greeting
{:input [:user/name [:? :user/title]]
:output [:user/greeting]}
[ctx {:user/keys [name title]}]
{:user/greeting (if title
(str "Hello, " title " " name "!")
(str "Hello, " name "!"))})Optional join inputs are also supported:
:input [:user/name {[:? :user/address] [:address/zip]}]You can also mark query items as optional. When a query item can't be resolved, it is simply omitted from the result instead of throwing:
(biff.graph/query {:biff.graph/index index}
{:user/id 1}
[:user/name [:? :user/nickname]])
;; => {:user/name "Alice"} ; :user/nickname omitted if no resolverOptional joins in queries:
[:user/name {[:? :user/extra] [:extra/info]}]Add :batch true to a resolver to make it accept a vector of input maps and return
a vector of output maps (in the same order). Batch resolvers are used automatically
when processing sequential join values (e.g. a list of friends):
(defn user-by-id
{:input [:user/id]
:output [:user/name :user/email]
:batch true}
[ctx inputs]
;; inputs is a vector of maps, e.g. [{:user/id 1} {:user/id 2}]
(mapv (fn [{:user/keys [id]}]
;; fetch from db in bulk...
{:user/name (str "User-" id)
:user/email (str "user" id "@example.com")})
inputs))Batch resolvers use breadth-first traversal: when a query has nested joins, all child entities across all parents at a given depth are collected and processed together. This means a batch resolver at depth N is called exactly once for all entities at that depth, regardless of how many parents exist.
For example, given this query:
[:a {:b [{:c [:d]}]}]If :b and :c resolve to vectors, the batch resolver for :d is called once
with ALL :c entities from ALL :b parents — not once per :b value.
Batch resolvers also work in single-entity contexts (the input is automatically wrapped in a vector and the result unwrapped).
You can also define resolvers as plain maps:
(def my-resolver
{:id :my-resolver
:input [:some/input]
:output [:some/output]
:resolve (fn [ctx input] {:some/output "value"})})
(def index (biff.graph/build-index [my-resolver]))query accepts either a single entity map or a vector of entity maps. When given
a vector, it returns a vector of result maps — and uses batch resolvers to process
all entities efficiently:
(biff.graph/query {:biff.graph/index index}
[{:user/id 1} {:user/id 2} {:user/id 3}]
[:user/name])
;; => [{:user/name "Alice"} {:user/name "Bob"} {:user/name "Carol"}]| Function | Description |
|---|---|
biff.graph/build-index |
Build an index from a collection of resolvers (vars or maps) |
biff.graph/query |
Run an EQL query: (query ctx entity-or-entities query-vec) |
biff.graph/resolver |
Normalize a resolver (var or map) into a resolver map |
The context map (ctx) passed to query must include :biff.graph/index
(the result of build-index). Any other keys in ctx are passed through to resolver
functions.
clojure -X:testMIT