Placeholders
Placeholders allow the user to add structure to some EQL request. They work as a special
attribute namespace. By default, Pathom recognize any attribute with the namespace >
as
a placeholder attribute.
You can configure the placeholder namespaces by setting the option com.wsscode.pathom3.placeholder/placeholder-prefixes
in the env, the default value is #{">"}
.
In this guide, you will go over a few examples of how to use placeholders.
Provide data
Pathom provides a plugin that allows you to provide data to the query. This is useful when you want to use Pathom to process a query, but you already have some data that you want to provide for that specific context.
Before version 2023.08.22-alpha
the provide data feature was a built-in feature. If you are upgrading, please make sure
to add the new plugin to your configuration.
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)
;; make sure you install the placeholder data plugin
(p.plugin/register (pbip/placeholder-data-params))))
(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"}}}
Compose different views
For this example, consider a system where you make a UI out of components, and each component has its query to express its data needs. Here are some of these component queries:
(def header-view-eql
[:acme.customer/full-name])
(def latest-orders-eql
[{:acme.customer/orders
[:acme.order/id
:acme.order/description]}])
(def address-eql
[:acme.address/street
:acme.address/number
:acme.address/zipcode])
One thing these queries have in common, is that they are "about" the same entity, always the same customer, but different data about it. You might be tempted to just concatenate the queries, something like:
(into [] (concat header-view-eql latest-orders-eql address-eql))
; [:acme.customer/full-name
; {:acme.customer/orders [:acme.order/id :acme.order/description]}
; :acme.address/street
; :acme.address/number
; :acme.address/zipcode]
Then run this query and send the full result to all the components, and for this specific example that would work, but I need to point some problems with this approach to you:
Conflict params
Let's add a new component to this system that breaks the previous solution, this is the new component:
(def expensive-orders-eql
[{'(:acme.customer/orders {::filter-price-gt 100})
[:acme.order/id
:acme.order/description]}])
Note this component uses the same attribute that latest-orders-eql
did, but it uses
EQL parameters to affect the request. Now we have two components that need the same
attribute but with different data. This is not possible and causes an attribute
conflict for the planner.
To have those two points of data in the same response, they need to be at different places.
Isolation break
Another problem with merging the queries is that the code gets data it didn't ask for. To illustrate this, let's imagine the following code to display the data from the address component:
(defn render-address [{:acme.address/keys [street number zipcode]}]
(str "Send to: " street ", " number ", " zipcode))
Now we also want to display the user name, so we change it to:
(defn render-address [{:acme.address/keys [street number zipcode]
:acme.customer/keys [full-name]}]
(str "Send to " full-name " at: " street ", " number ", " zipcode))
If we use the previous combined query, without changing the address-eql
to also ask for :acme.customer/full-name
, this
code "accidentally works", because header-view-eql
was asking for :acme.customer/full-name
. This can turn into
big headaches in the long run because by changing one component, you may affect other components.
Solving with placeholders
The solution to fix the previous issues is to put each component into a different placeholder path, this way each one gets full isolation from each other:
[{:>/header header-view-eql}
{:>/latest latest-orders-eql}
{:>/address address-eql}
{:>/expensive expensive-orders-eql}]
This way, Pathom guarantees isolation for each path, no more shared data, and each component can ask different params, and Pathom knows how to deal with it.