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
[(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}

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
[(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 revolvers 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"})
; => {:>/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"})
{'(:>/bard {::first-name "Bard"})
; {:>/bret
; {:com.wsscode.pathom3.docs.placeholder/full-name "Bret Victor",
; :>/bard
; {:com.wsscode.pathom3.docs.placeholder/full-name "Bard Victor"}}}

Placeholders data parameters is a new feature of Pathom 3, not available in Pathom 2.

Union queries

Union queries provide a way to archive polymorphism in with 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
[(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.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.

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?]}]
(if directory?
(mapv #(hash-map :file %) (.listFiles file))
(def file-env

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

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

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

(p.eql/process file-env
{:path "src"}
{: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:

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


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)
; 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
(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}

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

(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}}