Skip to content

Entities

Wilker Lúcio edited this page Sep 24, 2017 · 10 revisions

Abstract

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).

Using entity

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.

Map reader

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.

Understanding pathom joins

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]}])

Attribute dependency

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

Union queries

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.

Clone this wiki locally