Skip to main content

EQL

Using the EQL interface, you can request Pathom to fetch a specific shape of data.

If you are not familiar with EQL, check https://edn-query-language.org for an overview of the syntax.

The goal of using EQL is to express some data shape (hierarchy) without the values and let Pathom fill in the values.

Using EQL is also the most efficient way to request multiple things at once with Pathom. With EQL, Pathom knows the full request ahead of time. Therefore, Pathom can use this information to optimize the planning and execution.

Using EQL interface

Keep in mind that EQL is about expressing some data hierarchy, to start simple we will use a flat structure to demonstrate the basic usage of the EQL interface:

(ns com.wsscode.pathom.docs.eql-demos
(:require [com.wsscode.pathom3.connect.built-in.resolvers :as pbir]
[com.wsscode.pathom3.connect.indexes :as pci]
[com.wsscode.pathom3.interface.eql :as p.eql]))

(def indexes
(pci/register
[(pbir/constantly-resolver ::pi 3.1415)
(pbir/single-attr-resolver ::pi ::tau #(* % 2))]))

(p.eql/process indexes [::pi ::tau])
; => {::pi 3.1415 ::tau 6.283}

Errors

Check the error handling page to learn about error handling on EQL.

Nested entities

Using EQL joins you can make specific requirements about nested data. In this example we will simulate the existence of many worlds where PI have different values:

(def indexes
(pci/register
[(pbir/constantly-resolver ::pi 3.1415)
(pbir/single-attr-resolver ::pi ::tau #(* % 2))
; define a resolver to provide a collection of items
(pbir/constantly-resolver ::pi-worlds
[{::pi 3.14}
{::pi 3.14159}
{::pi 6.8}
{::tau 20}
{::pi 10 ::tau 50}])]))

(p.eql/process indexes
; using a map we are able to specify nested requirements from some attribute
[{::pi-worlds [::tau ::pi]}])
; => {::pi-worlds
; [{::tau 6.28
; ::pi 3.14}
; {::tau 6.28318
; ::pi 3.14159}
; {::tau 13.6
; ::pi 6.8}
; {::tau 20
; ::pi 3.1415}
; {::tau 50
; ::pi 10}]}

Providing map data

You can provide initial data to the EQL process using the following syntax:

(p.eql/process indexes {::pi 2.3} [::tau])
; => {::tau 4.6}

Providing data via EQL idents

Pathom uses the EQL ident as a form to specify a single attribute to start requesting data from. Here is an example using the resolvers we created before:

(p.eql/process indexes [{[::pi 2.3] [::tau]}])
; => {[::pi 2.3] {::tau 4.6}}

In this example, given PI is 2.3, Tau becomes 4.6, since it's defined as the double of PI.

Providing data with placeholders

You can use placeholders to provide in-query data for Pathom processing. To do this, lets get back to our famous full name example, the way to provide data is to send it to a placeholder key as EQL parameters:

(pco/defresolver full-name [{::keys [first-name last-name]}]
{::full-name (str first-name " " last-name)})

(def env (pci/register full-name))

(p.eql/process env
[{'(:>/bret {::first-name "Bret" ::last-name "Victor"})
[::full-name]}])
; => {:>/bret {:com.wsscode.pathom3.docs.placeholder/full-name "Bret Victor"}}

When moving to a placeholder context, Pathom inherits the same parent data and merges the params data to it, to illustrate let's make a nested example of it:

(p.eql/process env
[{'(:>/bret {::first-name "Bret" ::last-name "Victor"})
[::full-name
{'(:>/bard {::first-name "Bard"})
[::full-name]}]}])
; {:>/bret
; {:com.wsscode.pathom3.docs.placeholder/full-name "Bret Victor",
; :>/bard
; {:com.wsscode.pathom3.docs.placeholder/full-name "Bard Victor"}}}

Union queries

Union queries provide a way to achieve polymorphism in EQL, for a review on the union syntax refer to the EQL Union specification page.

Consider you want to request information for some user feed. In our feed example, there are three types of entries: posts, ads and videos. Each type requires different attributes to render. This is how we can write some resolvers to fetch each type:

(def union-env
(pci/register
[(pbir/static-table-resolver `posts :acme.post/id
{1 {:acme.post/text "Foo"}})
(pbir/static-table-resolver `ads :acme.ad/id
{1 {:acme.ad/backlink "http://marketing.is-bad.com"
:acme.ad/title "Promotion thing"}})
(pbir/static-table-resolver `videos :acme.video/id
{1 {:acme.video/title "Some video"}})
(pbir/constantly-resolver :acme/feed
[{:acme.post/id 1}
{:acme.ad/id 1}
{:acme.video/id 1}])]))

(p.eql/process union-env
[{:acme/feed
{:acme.post/id [:acme.post/text]
:acme.ad/id [:acme.ad/backlink :acme.ad/title]
:acme.video/id [:acme.video/title]}}])
; => {:acme/feed
; [{:acme.post/text "Foo"}
; {:acme.ad/backlink "http://marketing.site.com",
; :acme.ad/title "Promotion thing"}
; {:acme.video/title "Some video"}]}

To decide which path to take, Pathom looks if the entry data contains the key mentioned in the union entry key. When they match Pathom picks that path option.

There is a secondary option, this is more intended for implementors of dynamic resolvers.

If the meta contains the meta-data ::pf.eql/union-entry-key, Pathom will use that as the union entry key, example:

(def union-env
(pci/register
[(pbir/single-attr-resolver :acme.post/title :acme.post/link #(str % "-link"))
(pbir/single-attr-resolver :acme.ad/x :acme.ad/y inc)
(pbir/constantly-resolver :acme/feed
[^{:com.wsscode.pathom3.format.eql/union-entry-key :acme.post/id}
{:acme.post/title "TITLE"}
^{:com.wsscode.pathom3.format.eql/union-entry-key :acme.ad/id}
{:acme.ad/x 1}])]))

(p.eql/process union-env
[{:acme/feed
{:acme.post/id [:acme.post/link]
:acme.ad/id [:acme.ad/y]}}])
; => {:acme/feed
; [{:acme.post/link "TITLE-link"}
; {:acme.ad/y 2}]}

Recursive queries

Some data shapes are trees. For example, if we like to map a file system with Pathom.

I'll start writing a few resolvers to handle paths and directory navigation:

(pco/defresolver file-from-path [{:keys [path]}]
{:file (io/file path)})

(pco/defresolver file-name [{:keys [^File file]}]
{:file-name (.getName file)})

(pco/defresolver directory? [{:keys [^File file]}]
{:directory? (.isDirectory file)})

(pco/defresolver directory-files [{:keys [^File file directory?]}]
{::pco/output [{:files [:file]}]}
{:files
(if directory?
(mapv #(hash-map :file %) (.listFiles file))
::pco/unknown-value)})

(def file-env
(pci/register
[file-from-path
file-name
directory?
directory-files]))

To demonstrate the recursive property of it, I'll write the same nested query a few times to show it up:

(comment
(p.eql/process file-env
{:path "src"}
[:file-name
{:files [:file-name
{:files [:file-name
{:files []}]}]}]))

Instead of doing that, we can use EQL recursive queries to handle it:

(comment
(p.eql/process file-env
{:path "src"}
[:file-name
{:files '...}]))

The previous example creates an unbounded recursion. It's going to keep going until there is no more depth to go.

You can also limit this using bounded recursive queries:

(comment
(p.eql/process file-env
{:path "src"}
[:file-name
; max of 2 depths
{:files 2}]))

Wildcard

In EQL queries, you can use the special symbol * to ask Pathom to give all the data available for that entity. In other words, this removes the output filtering at that level. Here is an example of what it means:

; define a resolver that returns multiple things
(pco/defresolver user-data []
{:user/name "foo"
:user/email "some-user@email.com"
:user/birth-year 1988})

; standard query
(p.eql/process (pci/register user-data)
[:user/name])
; gets the output filtered, only the items in query show up
=> #:user{:name "foo"}

; making query adding the *
(p.eql/process (pci/register user-data)
[:user/name '*])
; now all the data that was loaded in process will show up in the result
=> #:user{:name "foo", :email "some-user@email.com", :birth-year 1988}

; another example, now we can see the whole deps showing up
(p.eql/process
(pci/register
[user-data
(pbir/single-attr-resolver :user/name :user/name++ #(str % " - extra things"))])
[:user/name++ '*])
=>
#:user{:name++ "foo - extra things",
:name "foo",
:email "some-user@email.com",
:birth-year 1988}
info

The * only affects sibling attributes (things at same entity/level), the following example illustrates it:

(p.eql/process
(pci/register user-data)
[{:>/ent1 [:user/name '*
{:>/nested [:user/email]}]}
{:>/ent2 [:user/birth-year]}])
=>
#:>{:ent1 {:user/name "foo",
:>/nested #:user{:email "some-user@email.com"},
:user/email "some-user@email.com",
:user/birth-year 1988},
:ent2 #:user{:birth-year 1988}}

Process One

Sometimes you just want a single value instead of a map of values as the output. The p.eql/process-one wrapper facilitates this use case:

Simplest usage:

(p.eql/process-one env :foo)

Same as process, you can send initial data:

(p.eql/process-one env {:data "here"} :foo)

You can also use joins and param expressions:

(p.eql/process-one env {:join [:sub-query]})
(p.eql/process-one env '(:param {:expr "sion"}))

Boundary Interface

The Pathom EQL design makes it a suitable solution to enable remote communication via EQL syntax.

By receiving an EQL request which is pure data, a server can fetch information and invoke mutations.

This is a powerful API, and it's something that Pathom is designed to handle.

Using the boundary interface

Using the boundary interface adds the following capabilities to the request:

  • Add support to load the index via EQL (through ::pci/indexes attribute)
  • Support provision of root entity data
  • Support requests in AST format
  • Strict mode errors are turned into a data format instead of throwing

Combined, those features allow another Pathom instance to integrate this graph.

The request to a boundary interface can be either an EQL request or a map. The map has the following options available:

; initial entity data
:pathom/entity {:foo "3"}

; the request in EQL format
:pathom/eql [:foo]

; the request in AST format
:pathom/ast {:type :root
:children [{:type :prop
:key :foo
:dispatch-key :foo}]}

; activate lenient mode, check error handling page for more details
; or set false to force strict mode
:pathom/lenient-mode? true

You should either use :pathom/eql or :pathom/ast. If both are present, Pathom will pick the AST.

Here you can see an example of a setup and usage of the boundary interface:

(ns com.wsscode.pathom3.docs.demos.core.eql
(:require
[com.wsscode.pathom3.interface.eql :as p.eql]
[com.wsscode.pathom3.connect.indexes :as pci]
[com.wsscode.pathom3.connect.built-in.resolvers :as pbir]
[com.wsscode.pathom3.connect.operation :as pco])
(:import (java.util Date)))

(pco/defresolver area [{:geo/keys [width height]}]
{:geo/area (* width height)})

(def env
(pci/register
[(pbir/constantly-fn-resolver ::now (fn [_] (Date.)))
area]))

(def pathom (p.eql/boundary-interface env))

; request EQL
(pathom [::now])

; send root entity data
(pathom {:pathom/entity {:geo/width 10 :geo/height 8}
:pathom/eql [:geo/area]})

; use AST, this way Pathom doesn't have to decode the EQL
(pathom
{:pathom/ast {:type :root
:children [{:type :prop
:key ::now
:dispatch-key ::now}]}})

It's recommended that you always use this at API boundaries.

Strict errors as data

info

Strict errors as data is feature from Pathom 3 2022.02.21-alpha+ versions.

When using the boundary interface, errors that would normally throw when using the EQL interface directly will be turned into a map format. This is more suitable to a boundary interface because then we can send this error reporting over the wire with ease.

The error format is opinionated, and it looks like this:

{:com.wsscode.pathom3.error/error-message "Pathom can't find a path for the following elements in the query: [:wrong] at path []",
:com.wsscode.pathom3.error/error-data {:com.wsscode.pathom3.connect.planner/graph {:com.wsscode.pathom3.connect.planner/nodes {},
:com.wsscode.pathom3.connect.planner/index-ast {:wrong {:type :prop,
:dispatch-key :wrong,
:key :wrong}},
:com.wsscode.pathom3.connect.planner/source-ast {:type :root,
:children [{:type :prop,
:dispatch-key :wrong,
:key :wrong}]},
:com.wsscode.pathom3.connect.planner/available-data {},
:com.wsscode.pathom3.connect.planner/unreachable-paths {:wrong {}}},
:com.wsscode.pathom3.connect.planner/unreachable-paths {:wrong {}},
:com.wsscode.pathom3.path/path [],
:com.wsscode.pathom3.error/phase :com.wsscode.pathom3.connect.planner/plan,
:com.wsscode.pathom3.error/cause :com.wsscode.pathom3.error/attribute-unreachable},
:com.wsscode.pathom3.error/error-stack "clojure.lang.ExceptionInfo: Pathom can't find a path for the following elements in the query: [:wrong] at path [] {:com.wsscode.pathom3.connect.planner/graph {:com.wsscode.pathom3.connect.planner/nodes {}, :com.wsscode.pathom3.connect.planner/index-ast {:wrong {:type :prop, :dispatch-key :wrong, :key :wrong}}, :com.wsscode.pathom3.connect.planner/source-ast {:type :root, :children [{:type :prop, :dispatch-key :wrong, :key :wrong}]}, :com.wsscode.pathom3.connect.planner/available-data {}, :com.wsscode.pathom3.connect.planner/unreachable-paths {:wrong {}}}, :com.wsscode.pathom3.connect.planner/unreachable-paths {:wrong {}}, :com.wsscode.pathom3.path/path [], :com.wsscode.pathom3.error/phase :com.wsscode.pathom3.connect.planner/plan, :com.wsscode.pathom3.error/cause :com.wsscode.pathom3.error/attribute-unreachable}
\tat com.wsscode.pathom3.connect.planner$verify_plan_BANG__STAR_.invokeStatic(planner.cljc:1510)
\tat com.wsscode.pathom3.connect.planner$verify_plan_BANG__STAR_.invoke(planner.cljc:1500)
\tat com.wsscode.pathom3.connect.planner$verify_plan_BANG_.invokeStatic(planner.cljc:1529)
\tat com.wsscode.pathom3.connect.planner$verify_plan_BANG_.invoke(planner.cljc:1522)
..."}

Then, clients on the receiving end can use this to report the error back.

Extending environment

The boundary API design is to make it flexible, and although is more efficient to initialize as much as possible before in the env, it still allows extension of it.

To extend the env you can send another argument to the boundary interface.

For example, let's say we have a resolver to get data from the current user. The current user is something we like to set at the start of the process, but we don't want to allow users to override it (for security reasons).

In this case we can set the user id on env when we call the boundary interface:

(def users-db
{1 {:user/login "bunny"}
2 {:user/login "fox"}})

(pco/defresolver current-user [{:keys [app/current-user-id]} _]
{::pco/output [:user/login]}
(get users-db current-user-id))

(def env (pci/register current-user))

(def pathom (p.eql/boundary-interface env))

(pathom {:app/current-user-id 1} [:user/login])
; => {:user/login "bunny"}

In this example we sent env as a map, the boundary interface gets this map and merges into the previous env and runs.

The env extension can also be a function, this allows more complex operations like registering new resolvers, for example:

(pathom
(fn [env]
(-> env
(assoc :app/current-user-id 2)
(pci/register (pbir/single-attr-resolver :user/login :user/greet #(str "Hello " %)))))
[:user/greet])
; => {:user/greet "Hello fox"}