-
-
Notifications
You must be signed in to change notification settings - Fork 62
Entities
Entities are one of the most important concepts to grasp about pathom. If you think of nodes on a graph, a node has its information and the connections with other nodes. Using this as a parallel, the entity
in pathom is the representation of the current node value; this is where you are taking the information to navigate on the graph. The entity is usually a Clojure map, but that's not a hard constraint, you can use whatever you want to represent the current entity, just take something from where you can derive the information from (although maps/map-like things a more straightforward support).
Pathom uses a commonplace to represent the entity in the environment. By default, it sits at ::p/entity
. Having it in a common place (instead of say, having a customer entity at a :customer
key) gives the opportunity to write generic readers to handle everyday situations (like the map-reader
we will see later).
To get the entity use the p/entity
function:
(ns com.wsscode.pathom-docs.using-entity
(:require [com.wsscode.pathom.core :as p]))
(defn read-attr [env]
(let [e (p/entity env)
k (get-in env [:ast :dispatch-key])]
(if (contains? e k)
(get e k)
::p/continue)))
(def parser
(p/parser {::p/plugins [(p/env-plugin {::p/reader [read-attr]})]}))
; we send the entity using ::p/entity key on environment
(parser {::p/entity #:character{:name "Rick" :age 60}} [:character/name :character/age :character/foobar])
; => #:character{:name "Rick", :age 60, :foobar :com.wsscode.pathom.core/not-found}
When you are on a node, you probably want to retrieve data from the node. Associating :dispatch-key -> map key
gets apparent soon. You have full access to env and entity, so be creative and fetch whatever you need.
Reading from maps matching the :dispatch-key
to the map key is very common, so we have a helper for that, let's take a look.
Let's re-write our previous example, now using the map-reader
:
(ns com.wsscode.pathom-docs.using-entity-map-reader
(:require [com.wsscode.pathom.core :as p]))
(def parser
(p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader]})]}))
; we send the entity using ::p/entity key on environment
(parser {::p/entity #:character{:name "Rick" :age 60}}
[:character/name :character/age :character/foobar])
; => #:character{:name "Rick", :age 60, :foobar :com.wsscode.pathom.core/not-found}
But our previous reader is naive about handling sequences and sub-queries, the map-reader
isn't, as we can see in the following example:
(ns com.wsscode.pathom-docs.using-entity-map-reader
(:require [com.wsscode.pathom.core :as p]))
(def parser
(p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader]})]}))
; we send the entity using ::p/entity key on environment
(parser {::p/entity #:character{:name "Rick" :age 60
:family [#:character{:name "Morty" :age 14}
#:character{:name "Summer" :age 17}]
:first-episode #:episode{:name "Pilot" :season 1 :number 1}}}
[:character/name :character/age
{:character/family [:character/age]}
{:character/first-episode [:episode/name :episode/number]}])
; =>
; #:character{:name "Rick",
; :age 60,
; :family [#:character{:age 14} #:character{:age 17}],
; :first-episode #:episode{:name "Pilot", :number 1}}
I encourage you to check the map-reader
implementation, it's not much longer than our previous one, and will give you a better understanding of how it runs.
Now that we saw some ways to work with the current entity, it's time to see how to navigate between them. You can look at the function p/join
as a way to set the current entity. The core principle of join
can be implemented as follows:
(defn join [entity {:keys [parser query] :as env}]
(parser (assoc env ::p/entity entity) query))
I start the parsing process again for a next node, using the sub-query. The difference on the pathom implementation is that it handles empty sub-query case (return the full entity) and handles the special *
value (so you can combine the whole entity + extra computed attributes). Pathom join
also handles union queries cases, more on that later.
The following example demonstrates how to use the map-reader
in combination with computed attributes and joins.
(ns com.wsscode.pathom-docs.using-entity-map-reader
(:require [com.wsscode.pathom.core :as p]))
; let's get rick into a variable
(def rick
#:character{:name "Rick"
:age 60
:family [#:character{:name "Morty" :age 14}
#:character{:name "Summer" :age 17}]
:first-episode #:episode{:name "Pilot" :season 1 :number 1}})
; an external data set so we can do a join
(def char-name->voice
{"Rick" #:actor{:name "Justin Roiland" :nationality "US"}
"Morty" #:actor{:name "Justin Roiland" :nationality "US"}
"Summer" #:actor{:name "Spencer Grammer" :nationality "US"}})
; this is our computed attributes, stuff to look for when the entity doesn't contain the requested
; attribute
(def computed
{:character/voice
(fn [env]
(let [{:character/keys [name]} (p/entity env)
voice (get char-name->voice name)]
(p/join voice env)))})
(def parser
; note we have both readers now, map and computed
(p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader computed]})]}))
(parser {::p/entity rick}
'[:character/name
; the join enables us to query exactly what we need from the node
{:character/voice [:actor/name]}
; two new things going on here, the * will ask for all attributes on the family nodes
; also by not specifying the query for :character/voice it will return the entity itself
{:character/family [* :character/voice]}])
So far we demonstrated how to fetch entity information and how to compute information from it. What if we need a computed information that depends on another computed information? To materialize this idea, I'll give a real example I had at work. Say you need to get a list of
Sometimes we need to handle heterogeneous nodes, nodes that depending on its type you want a different query. Union queries solve these cases. A common place for union queries are searching, let's see an example where a search can be a user, a movie or a book.
(ns pathom-docs.entity-union
(:require [com.wsscode.pathom.core :as p]))
(def search-results
[{:type :user
:user/name "Jack Sparrow"}
{:type :movie
:movie/title "Ted"
:movie/year 2012}
{:type :book
:book/title "The Joy of Clojure"}])
(def parser
(p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader]})]}))
(parser {::p/entity {:search search-results}
; here we set where pathom should look on the entity to determine the union path
::p/union-path :type}
[{:search {:user [:user/name]
:movie [:movie/title]
:book [:book/title]}}])
The extra we need to care about when doing unions is that we need to have a way to determine which path to go. On the previous example, we use the :type
(a key on the entity) to determine which branch to follow. The value of ::p/union-path
can be a keyword (from something inside entity or a computed attribute) or a function (will take env
).
If you want to the ::p/union-path
to be more contextual you can set it during the join process, as in the next example:
(ns pathom-docs.entity-union-contextual
(:require [com.wsscode.pathom.core :as p]))
(def search-results
[{:type :user
:user/name "Jack Sparrow"}
{:type :movie
:movie/title "Ted"
:movie/year 2012}
{:type :book
:book/title "The Joy of Clojure"}])
(def search
{:search
(fn [env]
; join-seq is the same as join, but for sequences, note we set the ::p/union-path
; here this time, so we can be sure this will be used on this context, and then
; you can use different ones depending on the list in case
(p/join-seq (assoc env ::p/union-path :type) search-results))})
(def parser
(p/parser {::p/plugins [(p/env-plugin {::p/reader [search
p/map-reader]})]}))
(parser {}
[{:search {:user [:user/name]
:movie [:movie/title]
:book [:book/title]}}])
This is something beautiful about having an immutable environment; you can make changes with confidence that it will not affect indirect points of the parsing process.