Liking cljdoc? Tell your friends :D

Pathom Developers Guide

Table of Contents

1. Introduction

The pathom library provides a rich set of functionality to build robust parsers to process graph queries for Om.next and Fulcro. The query notation is derived from Datomic pull syntax.

The library includes:

  • A reader abstraction that allows for easy composition.

  • The concept of entity which works as a base framework for reusable sharable readers.

  • A plugin system with some built-in plugins:

    • Error handler: Handles errors at an attribute level.

    • Request cache: For caching the results of parsing repetition that can happen on a single request.

    • Profiler: a plugin to measure the time spent on each attribute during the parser.

  • Connect: a higher level abstraction that can resolve attribute relationships automatically. For example automatic traversal of database joins or resolving data through network requests. This enables exploratory capabilities and much simpler access when the need arises to do extra work to resolve a single conceptual join.

  • GraphQL integration: Use GraphQL endpoints directly from your query system (in development).

Most people will find the most leverage in the "Connect" features, which allow you to quickly build dynamic query processing systems to easily satisfy client data requests.

1.1. Aliases Used in Code Examples

Throughout the book our code examples will use aliases instead of explicit namespaces. The aliases used are as if we had the following namespace requires:

(ns my-namespace
  (:require
    [com.wsscode.pathom.connect :as pc]
    [com.wsscode.pathom.graphql :as pg]
    [com.wsscode.pathom.connect.graphql :as pcg]
    [com.wsscode.common.async-cljs :refer [let-chan <!p go-catch <? <?maybe]]
    [com.wsscode.pathom.core :as p]))

So, any time you see a usage of a namespace in a keyword or function like p/parser or ::p/reader you should remember that these are the namespaces involved.

1.2. Presentation

If you like to learn by seeing presentations, I did one at Dutch Clojure Days 2018.

1.3. Contributing

This source for this book is at https://github.com/wilkerlucio/pathom/blob/master/docs/DevelopersGuide.adoc. Feel free to send a PR with edits, corrections, or other contributions. If you’re wanting to make a large edit, please open an issue first.

2. How to Use This Library

We expect that most of our user base is made up of Om Next or Fulcro users. The purpose of this library is to make it much easier to build code that can process the graph query notation of these libraries on both the client and server side. We expect you to have have one or more of the following needs:

  • You want to fulfill a client UI query from some server-side data source(s).

  • You want to build a client-side parser for directly filling local UI data queries from a local data source.

  • You want to build a parser (client or server) that uses async APIs to fulfill different parts of a query. Perhaps gluing together data sources from various micro-services.

  • You want to use a GraphQL API from the client.

  • You want to provide third-party users a GraphQL API (Future Work)

When building most parsers you’ll want to use Pathom Connect.

To process Fulcro/Om Next queries against GraphQL you’ll use the GraphQL Integration.

3. Pathom Connect

Connect provides is a high-level abstraction layer for building query processing code. It handles a number of the gory details in an automatic way that allows you to focus more on your data model and less on the parser itself. It generates an an index of your graph’s features that can be used for a number of very useful features:

  1. Auto-complete of graph queries in tools for data exploration (see OgE).

  2. Graph edge generation from the index’s connection information.

  3. Multiple ways to reach a given attribute, automatically resolved via known reachable edges and transitive relations.

The Connect index is a rich source of information about how your attributes connect and how they can locate each other.

3.1. The Basics

In order to use connect you need to understand some basics about Pathom’s core features. These are covered in detail in later chapters, but you’ll easily understand the basics we need for connect without going into great detail.

You’re going to be defining a parser that uses an environment and graph query to produce a tree of data from arbitrary data sources.

If you’re unfamiliar with the Om Next/Fulcro graph query notation, you should first read up on that in the Fulcro Developer’s Guide.

Some simple examples of what we’re doing are:

;; query
[:person/name]

;; possible result
{:person/name "Samantha"}

;; query
[:account/id
 {:billing/charges [:charge/amount]}]

;; possible result
{:account/id 1
 :billing/charges [{:charge/amount 11}
                   {:charge/amount 22}]}

To make sure we’re on the same page, here’s a quick bit of vocabulary:

Environment

A map of configuration and context that is used to configure the parser and is also passed to the reader.

Resolver

A Pathom Connect component that you write to indicate ways in which data attributes can be resolved against your data sources. Resolvers are composed together into a connect-based Reader.

Reader

A component of that attempts to resolve elements of the query (one at a time). When using connect you also use a built-in Map Reader that can pull attributes that are already in the environment without having to do further work against your underlying resolvers and data sources.

Connect Indexes

A set of indexes that are filled with resolver data and allow connect to understand how graph queries can be resolved by the resolvers.

3.2. Baseline Boilerplate

Connect is generally set up with the minimal steps every time. Other sections of this book cover the options in more detail, but for the moment take this small bit of code as the general "starting point" for writing a connect-based query processing system:

(ns com.wsscode.pathom.book.connect.getting-started
  (:require [com.wsscode.pathom.core :as p]
            [com.wsscode.pathom.connect :as pc]))

;; create a multimethod dispatch function.
(defmulti resolver-fn pc/resolver-dispatch)

;; Create a place to store the connect indexes
(def indexes (atom {}))

;; Creates a factory for building your "data resolvers"
(def defresolver (pc/resolver-factory resolver-fn indexes))

;; Define one or more resolvers
(defresolver `some-symbol ...)
(defresolver `some-other-symbol ...)
...

;; Create a parser that uses the resolvers:
(def parser (p/parser {::p/env {::p/reader             [p/map-reader pc/all-readers]
                                ::pc/resolver-dispatch resolver-fn
                                ::pc/indexes           @indexes}}))

3.3. Resolvers

In Connect you implement the graph by creating resolvers, those resolvers are functions that expose some data on the graph.

A resolver has a few basic elements:

  1. Inputs – A set of attributes that must be in the current parsing context for the resolver to be able to work. Inputs is optional, and missing inputs means that the resolver is always capable of working independent of the current parsing context.

  2. Outputs - A query-like notation that gives the "pattern" of the part of the query the resolver is able to resolve. This is typically a list of attributes/joins, where joins typically include a simple subquery.

  3. A lambda - A (fn [env input-data] tree-of-promised-output) that takes the inputs and turns them into a tree that satisfies the "output query".

So you might define a resolver like this:

(defresolver `person-resolver
  {::pc/input #{:person/id}
   ::pc/output [:person/first-name :person/age]}
  (fn [{:keys [database] :as env} {:keys [person/id]}]
    (let [person (my-database/get-person database id)]
      {:person/age (:age person)
       :person/first-name (:first-name person)})))

Where the database in the environment would be supplied when running the parser, and the input would have to be found in the current context. Remember that graph queries are contextual…​you have to have a starting node to work from, so in the above example we’re assuming that during our parse we’ll reach a point where the context contains a :person/id. The my-database stuff is just made up for this example, and is intended to show you that your data source need not remotely match the schema of your graph query.

Pathom will scan through the defined resolvers in order to try to satisfy all of the properties in a query. So, technically you can split up your queries as much as makes sense into separate resolvers, and as long as the inputs are in the context Pathom will assemble things back together.

Of course, it doesn’t make sense in this case to do so, because each resolver would end up running a new query:

(defresolver `person-age-resolver
  {::pc/input #{:person/id}
   ::pc/output [:person/age]}
  (fn [{:keys [database] :as env} {:keys [person/id]}]
    (let [person (my-database/get-person database id)]
      {:person/age (:age person)})))

(defresolver `person-first-name-resolver
  {::pc/input #{:person/id}
   ::pc/output [:person/first-name]}
  (fn [{:keys [database] :as env} {:keys [person/id]}]
    (let [person (my-database/get-person database id)]
      {:person/first-name (:first-name person)})))

...

The point is that a single-level query like [:person/id :person/first-name :person/age] can be satisfied and "folded together" by pathom over any number of resolvers.

This fact is the basis of parser (de)composition and extensibility. It can also come in handy for performance refinements when there are computed attributes.

3.3.1. Derived/Computed Attributes

There are times when you’d like to provide an attribute that is computed in some fashion. You can, of course, simply compute it within the resolver along with other properties like so:

(defresolver `person-resolver
  {::pc/input #{:person/id}
   ::pc/output [:person/first-name :person/last-name :person/full-name :person/age]}
  (fn [{:keys [database] :as env} {:keys [person/id]}]
    (let [{:keys [age first-name last-name]} (my-database/get-person database id)]
      {:person/age age
       :person/first-name first-name
       :person/last-name last-name
       :person/full-name (str first-name " " last-name) ; COMPUTED
       ... })))

but this means that you’ll take the overhead of the computation when any query relate to person comes up. You can instead spread such attributes out into other resolvers as we discussed previously, which will only be invoked if the query actually asks for those properties:

(defresolver `person-resolver
  {::pc/input #{:person/id}
   ::pc/output [:person/first-name :person/last-name :person/age]}
  (fn [{:keys [database] :as env} {:keys [person/id]}]
    (let [{:keys [age first-name last-name]} (my-database/get-person database id)]
      {:person/age age
       :person/first-name first-name
       :person/last-name last-name })))

(defresolver `person-name-resolver
  {::pc/input #{:person/first-name :person/last-name}
   ::pc/output [:person/full-name]}
  (fn [_ {:person/keys [first-name last-name]}]
    {:person/full-name (str first-name " " last-name)}))

This combination of resolvers can still resolve all of the properties in [:person/full-name :person/age] (if :person/id is in the context), but a query for just [:person/age] won’t invoke any of the logic for the person-name-resolver.

3.3.2. Single Inputs — Establishing Context

So far we have seen how to define a resolver that can work as long as the inputs are already in the environment. You’re almost certainly wondering how to do that.

One way is to define global resolvers and start the query from them, but very often you’d just like to be able to say "I’d like the first name of person with id 42."

Om Next/Fulcro use "idents" to specify exactly that sort of query:

[{[:person/id 42] [:person/first-name]}]

The above is a join on an ident, and the expected result is a map with the ident as a key:

{[:person/id 42] {:person/first-name "Joe"}}

The query itself has everything you need to establish the context for running the person-resolver, and in fact that is how Pathom single-input resolvers work.

If you use an ident in a query then Pathom is smart enough to know that it can use that ident to establish the context for finding resolvers. In other words, in the query above the ident [:person/id 42] is turned into the parsing context {:person/id 42}, which satisfies the input of any resolver that needs :person/id to run.

3.3.3. Resolver Without Input — Global Resolver

A resolver that requires no input can output its results at any point in the graph, thus it is really a global resolver. Pay particular note to the qualfication: any point in the graph. Not just root. Thus, a resolver without inputs can "inject" its outputs into any level of the query graph result.

We’re going to start building a parser that can satisfy queries about a music store. So, we’ll start with a global resolver that can resolve the "latest product". The code below shows the entire code needed, boilerplate and all:

link:../src-docs/com/wsscode/pathom/book/connect/getting_started.cljs[role=include]

Our first resolver exposes the attribute ::latest-product, and since it doesn’t require any input it is a global resolver. Also, note that our output description includes the full output details (including nested attributes), this is mostly useful for auto-complete on UI’s and automatic testing. If you return extra data it will still end up in the output context.

Try some of these queries on the demo below:

[::latest-product]
[{::latest-product [:product/title]}]

; ::latest-product can be requested anywhere
[{::latest-product
  [* ::latest-product]}]
[::latest-product]

3.3.4. Resolvers with input

Next, let’s say we want to have a new attribute which is the brand of the product. Of course, we could just throw the data there in our other resolver, but the real power of connect comes out when we start splitting the responsibilities among resolvers, so let’s define a resolver for brand that requires an input of :product/id:

link:../src-docs/com/wsscode/pathom/book/connect/getting_started2.cljs[role=include]
[{::latest-product [:product/title :product/brand]}]

The input is a set containing the keys required on the current entity in the parsing context for the resolver to be able to work. This is where Connect starts to shine because any time your query asks for a bit of data it will try to figure it out how to satisfy that request based on the attributes that the current contextual entity already has.

More importantly: Connect will explore the dependency graph in order to resolve things if it needs to! To illustrate this let’s pretend we have some external ID for the brand, and that we can derive this ID from the brand string, pretty much just another mapping:

;; a silly pretend lookup
(def brand->id {"Taylor" 44151})

(defresolver `brand-id-from-name
 {::pc/input #{:product/brand}
  ::pc/output [:product/brand-id]}
 (fn [_ {:keys [product/brand]}]
   {:product/brand-id (get brand->id brand)}))

(comment
  (parser {} [{::latest-product [:product/title :product/brand-id]}])
  ; => #::{:latest-product #:product{:title "Acoustic Guitar", :brand-id 44151}}
)

Note that our query never said anything about the :product/brand. Connect automatically walked the path :product/id → :product/brand → :product/brand-id to obtain the information desired by the query!

When a required attribute is not present in the current entity, Connect will look for resolvers that can fetch it, analyze their inputs, and recursively walk backwards towards the "known data" in the context. This is what makes Connect powerful: by leveraging the index containing the attribute relationships you can focus on writing just the primary edges of the graph, and then all possible paths can be walked automatically. You can read more about how this works in the Index page.

In case the path is a dead end (not enough data), Connect triggers an error explaining the miss. Let’s see that in action:

(parser {} [:product/brand])
; CompilerException clojure.lang.ExceptionInfo: "Insufficient resolver output" {::pp/response-value {...} :key :attribute}

As you can see, Connect will fire an error in case your query asks for something and it’s not possible to get there.

Also remember that single-input resolvers can handle ident-based queries. Thus, the following ident-join queries already work without having to define anything else:

(parser {} [{[:product/id 1] [:product/brand]}])
; => {[:product/id 1] #:product{:brand "Taylor"}}

(parser {} [{[:product/brand "Taylor"] [:product/brand-id]}])
; => {[:product/brand "Taylor"] #:product{:brand-id 44151}}

3.3.5. Multiple inputs

The input to a resolver is a set, and as such you can require more than one thing as input to your resolvers. When doing so, of course, your resolver function will receive all of the inputs requested; however, this also means that the parsing context needs to contain them, or there must exist other resolvers that can use what’s in the context to fill them in.

3.3.6. N+1 Queries and Batch resolvers

When you have a to-many relation that is being resolved by a parser you will typically end up with a single query that finds the "IDs", and then N more queries to fill in the details of each item in the sequence. This is known as the N+1 problem, and can be a source of significant performance problems.

The solution is relatively simple in Pathom: Add the request-cache-plugin to the parser and include a ::pc/batch true option in your resolver. Pathom then knows that it can send a sub-resolver a sequence of inputs instead of a single one. If your resolver receives a sequence, then it is expected to return a vector of promised outputs instead of a single map.

You must detect if the input is a sequence and also be ready to handle the case when it is not.

Let’s see one example to illustrate the situation:

This example is using Pathom async parsers. The resolvers in async parsers can return channels that (eventually) resolve to the result, which is why you see go blocks in the code. See Async Parsing for more details. We use them in this example so we can "sleep" in a Javascript environment to mimic overhead in processing.
link:../src-docs/com/wsscode/pathom/book/connect/batch.cljs[role=include]

Try the example:

[{:items [:number-added]} :com.wsscode.pathom.profile/profile]

You can note by the profiler that it took one second for each entry, because it had to call the :number-added resolver once for each item.

We can improving that by turning this into a batch resolver, like this:

link:../src-docs/com/wsscode/pathom/book/connect/batch2.cljs[role=include]

Try the example:

[{:items [:number-added]} :com.wsscode.pathom.profile/profile]

Note that this time the sleep of one second only happened once, this is because when Pathom is processing a list and the resolver supports batching, the resolver will get all the inputs in a single call, so your batch resolver can get all the items in a single iteration. The results will be cached back for each entry, this will make the other items hit the cache instead of calling the resolver again.

Note the request-cache plugin is required for the batch to work.

3.4. Connect mutations

Using mutations from connect will give you some extra leverage by adding the mutation information to the index, this will enable auto-complete features for explorer interfaces, and also integrates the mutation result with the connect read engine.

3.4.1. Mutations setup

The mutation setup looks very much like the one from resolvers, Pathom provides a factory builder function so you can create mutations with ease. Here is some example setup code:

(ns com.wsscode.pathom.book.connect.mutations
  (:require [com.wsscode.pathom.connect :as pc]
            [com.wsscode.pathom.core :as p]))

; setup indexes atom
(def indexes (atom {}))

; setup mutation dispatch and factory
(defmulti mutation-fn pc/mutation-dispatch)
(def defmutation (pc/mutation-factory mutation-fn indexes))

(def parser
  (p/parser {::p/plugins [(p/env-plugin
                            {::pc/mutate-dispatch mutation-fn
                             ::pc/indexes         @indexes})]
             :mutate     pc/mutate}))

Now let’s write a mutation with our factory.

3.4.2. Creating mutations

The defmutation have the same interface that we used with defresolver.

link:../src-docs/com/wsscode/pathom/book/connect/mutations.cljs[role=include]
[(send-message {:message/text "Hello Clojurist!"})]

The ::pc/params is currently a non-op, but in the future it can be used to validate the mutation input, it’s format is the same as output (considering the input can have a complex data shape). The ::pc/output is valid and can be used for auto-complete information on explorer tools.

Mutation joins

After doing some operation, you might want to read information about the operation result. With connect you can leverage the resolver engine to expand the information that comes from the mutation. To do that you do a mutation join, and use that to query the information. Here is an example where we create a new user and retrieve some server information with the output.

link:../src-docs/com/wsscode/pathom/book/connect/mutation_join.cljs[role=include]
[{(user/create {:user/name "Rick Sanches" :user/email "rick@morty.com"}) [:user/id :user/name :user/created-at]}]

Note that although we only return the :user/id from the mutation, the resolvers can walk the graph and fetch the other requested attributes.

Mutation join globals

Some attributes need to be in the output, even when they are not asked for. For example, if your parser is driving a Fulcro app, the :tempid part of the mutation will be required for the app to remap the ids correctly. We could ask for the user to add it on every remote query, but instead we can also define some global attributes and they will be read every time. As in this example:

link:../src-docs/com/wsscode/pathom/book/connect/mutation_join_globals.cljs[role=include]
[{(user/create {:user/id "TMP_ID" :user/name "Rick Sanches" :user/email "rick@morty.com"}) [:user/id :user/name :user/created-at]}]

So in case of fulcro apps you can use the :fulcro.client.primitives/tempids as the global and have that pass though.

3.4.3. Async mutations

If we need async support, connect mutations are ready for it. Instead of using the p/mutate method, switch to p/mutate-async.

Example:

link:../src-docs/com/wsscode/pathom/book/connect/mutation_async.cljs[role=include]
[{(user/create {:user/id "TMP_ID" :user/name "Rick Sanches" :user/email "rick@morty.com"}) [:user/id :user/name :user/created-at]}]

Using the same query/mutation interface, we replaced the underlying implementation from an atom to a indexeddb database.

You can do the same to target any type of API you can access.

3.5. Shared resolvers

Since version 2.2 Pathom adds support to describe resolvers as pure maps and register those resolvers in your system, making possible to write easy to share resolvers in a library format.

3.5.1. Resolver data format

Here is an example of how you can specific a resolver using the map format:

(def some-resolver
  {::pc/input   #{:customer/id}
   ::pc/output  [:customer/id :customer/name :customer/email]
   ::pc/resolve (fn [env input] ...)})

As you can see it’s very similar to using defresolver, you just add the key ::pc/resolve to define the runner function of it.

3.5.2. Mutation data format

Mutations are similar as well:

(def send-message-mutation
  {::pc/params #{:message/body}
   ::pc/output [:message/id :message/body :message/created-at]
   ::pc/mutate (fn [env params] ...)})

As you can see it’s very similar to using defresolver, you just add the key ::pc/resolve to define the runner function of it.

3.5.3. Using register

Once you have your maps ready you can register then using the connect register function.

(def indexes (atom {}))

(defmulti resolver-fn pc/resolver-dispatch)
(def defresolver (pc/resolver-factory resolver-fn indexes))

(defmulti mutation-fn pc/mutation-dispatch)
(def defmutation (pc/mutation-factory mutation-fn indexes))

(def reg-config {::pc/defresolver defresolver ::pc/defmutation})

; register the resolver we created previously
(pc/register reg-config some-resolver)

; same method works for mutations
(pc/register reg-config send-message-mutation)

; you can also send collections to register many at once
(pc/register reg-config [some-resolver send-message-mutation])

; collections will be recursivelly processed, so this is valid too:
(pc/register reg-config [some-resolver [send-message-mutation]])

If you are a library author, consider defining each resolver/mutation as its own symbol and then create another symbol that is vector combining your features, this way you make easy for your users to just get the vector, but still allow then to cherry pick which operations he wants to pull if he doesn’t want it all.

3.5.4. Plugins with resolvers

It’s also possible for plugins to declare resolvers and mutations so they get installed when the plugin is used. To do that your plugin must provide the ::pc/resolvers key on the plugin map, and you also need to use the pc/connect-plugin, this plugin will make the instalation, here is an example:

...

(def my-plugin-with-resolvers
 {::pc/resolvers [some-resolver send-message-mutation]})

(def parser
  (p/parser {::p/env          (fn [env]
                                (merge
                                  {::p/reader             [p/map-reader pc/reader pc/open-ident-reader]
                                   ::pc/resolver-dispatch resolver-fn
                                   ::pc/mutate-dispatch   mutation-fn
                                   ::pc/indexes           @indexes}
                                  env))
             ::pc/defresolver defresolver
             ::pc/defmutation defmutation
             ::p/mutate       pc/mutate-async
             ::p/plugins      [pc/connect-plugin ; make sure connect-plugin is here, it's order doesn't matter
                               my-plugin-with-resolvers]}))

And that’s it, the resolvers will be instead right after the parser is defined.

3.6. Advanced Connect Details

3.6.1. Connect readers

The p.connect/all-readers is a combination of a few readers used by connect, let’s talk about each reader individually.

p.connect/reader

The main Connect reader. This will look up the attribute in the index and try to resolve it, recursively if necessary.

p.connect/ident-reader

The ident-reader is used to resolve ident-based queries by establishing an initial context from the ident. When an ident query reaches this reader it will check the index to see if the ident key is present on in the indexed idents.

Since version 2.2.0-beta11 this reader also supports extra context provision using the param :pathom/context, here is how to send extra data to it:

[{([:user/id 123] {:pathom/context {:other/data 123}})
  [:user/id :user/name :other/data]}]
p.connect/open-ident-reader

Like ident-reader, but not constrained to the indexed idents, this will create a context from any ident.

p.connect/index-reader

This reader exposes the index itself with the name ::p.connect/indexes.

3.6.2. Understanding the indexes

Connect maintains a few indexes containg information about the resolvers and the relationships on attributes. Connect will look up the index in the environment, on the key :com.wsscode.pathom.connect/indexes, which is a map containing the indexes

In order to explain the different indexes we’ll look at the index generated by our example in the getting started section:

{::p.connect/index-resolvers
 {get-started/latest-product
  {::p.connect/sym    get-started/latest-product
   ::p.connect/input  #{}
   ::p.connect/output [{::get-started/latest-product [:product/id
                                                      :product/title
                                                      :product/price]}]}

  get-started/product-brand
  {::p.connect/sym    get-started/product-brand
   ::p.connect/input  #{:product/id}
   ::p.connect/output [:product/brand]}

  get-started/brand-id-from-name
  {::p.connect/sym    get-started/brand-id-from-name
   ::p.connect/input  #{:product/brand}
   ::p.connect/output [:product/brand-id]}}

 ::p.connect/index-oir
 {:get-started/latest-product {#{} #{get-started/latest-product}}
  :product/brand              {#{:product/id} #{get-started/product-brand}}
  :product/brand-id           {#{:product/brand} #{get-started/brand-id-from-name}}}

 ::p.connect/index-io
 {#{}               {:get-started/latest-product #:product{:id {} :title {} :price {}}}
  #{:product/id}    {:product/brand {}}
  #{:product/brand} {:product/brand-id {}}}

 ::p.connect/idents
 #{:product/brand :product/id}}
index-resolvers

This is a raw index of available resolvers, it’s a map resolver-sym → resolver-data. resolver-data is any information relevant that you want to add about that resolver. Any key that you adding during p.connect/add will end up on this map, also Connect will add the key ::p.connect/sym automatically, which is the same symbol you added. If you want to access the data for a resolver, Connect provides a helper function for that:

(p.connect/resolver-data env-or-indexes `product-brand)
; => {::p.connect/sym    get-started/product-brand
;     ::p.connect/input  #{:product/id}
;     ::p.connect/output [:product/brand]}
index-oir

This index stands for output → input → resolver. It’s the index used for the Connect reader to look up attributes. This index is built by looking at the input/output for the resolver when you add it. It will save that resolver as a path to each output attribute, given that input. It basically inverts the order of things: it keys the output attribute to all of the potential "starting points".

Let’s do an exercise and see how connect traverses this index in a practical example:

Given we have this index (oir):

link:../src-docs/com/wsscode/pathom/book/connect/index_oir_example.cljc[role=include]

Now if you try to run the query:

[:name]

So we look in the index for :name, and we get {#{:id} #{thing-by-id}}, now we try to match the current entity attribute keys with the sets to see if we have enough data to call any of them. If we don’t it will fail because we don’t have enough data.

[{[:id 123] [:name]}]

So, if we start with an ident, our initial context is {:id 123}. This time we have the :id, so it will match with the input set #{:id}, and will call the resolver thing-by-id with that input to figure out the name. Connect uses atom entities: when it gets the return value from the resolver it merges it back into the context entities, making all data returned from the resolver available to access new attributes as needed.

index-io

The auto-complete index, input → output. This index accumulates the reach for each single attribute on the index. By walking this information we can know ahead of time all attribute possibilities we can fetch from a given attribute.

If I have a :product/id, what can I reach from it? Looking at the index, the :product/id itself can provide the :product/brand. But if I have access to :product/brand it means I also have access to whatever :product/brand can provide. By doing multiple iterations (until there are no new attributes) we end up knowing that :product/id can provide the attributes :product/brand and :product/brand-id. And this is how autocomplete is done via the index-io.

idents

The idents index contain information about which single attributes can be used to access some information. This index is used on ident-reader and on OgE to provide auto-complete options for idents. Any time you add a resolver that has a single input, that input attribute is added on the idents index.

autocomplete-ignore

This index is for a more advanced usage. Currently it’s only used by the GraphQL integration. In the GraphQL integration we leverage the fact that types have a fixed set of attributes and add that into the index. The problem is that the types thenselves are not valid entries for the query, then autocomplete-ignore is a way to make those things be ignored in the auto-complete. You probably only need this if you are building the index in some custom way.

4. Getting Started

In this chapter we’ll give you some basics for using the library to parse arbitrary queries. In order to understand this book you should understand the syntax of these queries. This chapter will touch on some basics. More details can be found in the Om Next documentation and in the Fulcro Developer’s Guide

For now, you can get by with just a short introduction:

4.1. Query Notation Introduction

A query is a vector that lists the items you want. A keyword requests a scalar (opaque) value, and a map indicates a to-many or to-one join (resolved at runtime using database content).

Queries are always "relative" to some starting context (which is typically supplied via parameters or by a top-level join).

If you want to obtain the name and age of "some" person:

[:person/name :person/age]

If you want to obtain a person’s name and the street of their address you might write this:

[:person/name {:person/address [:address/street]}]

where we imagine that the underlying database has some kind of normalization that needs to be traversed in order to satisfy the address data.

The result of running a query is a map containing the result (in the same recursive shape as the query):

Running [:person/name :person/age] against the person "Sam" might give:

{:person/name "Sam" :person/age 32}

Running [:person/name {:person/address [:address/street]}] against that same person might give:

{:person/name "Sam" :person/address {:address/street "111 Main St."}}

The query establishes the request and expectation. Interpreting and satisfying these queries from some arbitrary data source is the job of a query parser/interpreter. This library gives you tools for quickly building the latter.

4.2. Parsing Context

The elements of a graph query are relative: they have a contextual meaning. If you ask for a person’s name, the implication is that you are querying a "person entity"; however, the other required bit of information is which person. Thus, elements of a query cannot be fulfilled they are rooted in a context. This applies to joins as well (e.g. what is the current person’s address?), but once you’ve resolved the context of the root of some graph query the joins simply describe navigation from that context (the person) to another (their address) via a relation that is either already described in the underlying data source itself, or in code you provide that can figure it out.

As the parser moves through a query like [:person/name {:person/address [:address/street]}] it first starts with some context (e.g. "Sam"). When it finds a join it processes the subquery against a new context (e.g. Sam’s address) to give the result:

{:person/name "Sam" :person/address {:address/street "111 Main St."}}

So, there is always a context at any given point when parsing a query. This context is either established at startup by resolving a specific entity, or is the entity (or entities if to-many) that have been reached by processing the joins of the query.

4.3. Parsing Environment and The Reader

The parsing environment is simply a map that carries along data while parsing (and can be augmented as you go). It establishes the meaning of the "current context", can contain anything you wish (via namespaced keywords), and can be seen in any code that you plug in to process the query.

There are some predefined (namespaced) keys that have special meaning to the parser. In particular :com.wsscode.pathom.core/reader can be used to supply reader(s) for the parser to use. The reader can be a map from attributes to functions, a plain function, or even a vector of functions. It is asked to read the value for the elements of the query using the current environment. We’ll expand on that as we go, or you can read more in the Readers section.

4.4. A Trivial Parser

Our first example will be for a very simple parser that can resolve a single scalar property using a function that you define:

(ns pathom-docs.hello-pathom
  (:require [com.wsscode.pathom.core :as p]))

; Our first reader, defined with a map whose keys are the dispatch key from the query,
; and whose value is a function to resolve the data to return.
(def computed
  ; here we define that for the dispatch-key :hello we are going to return "World"
  {:hello (fn [env] "World")})

; Create a parser
(def parser (p/parser {}))

; Run the parser, using the reader, against a simple query (arguments are env and query):
(parser {::p/reader computed} [:hello])
; => {:hello "World"}

4.5. Types of Reads

When parsing a graph API there are 3 major types of reading that you want to do at any level:

  1. Entity attributes: Attributes that are already present on the current entity (the current context). For example, if we have already loaded (and are at) a customer node, it might have attributes like :customer/id and :customer/name. When the query asks for those we can fetch them from the entity itself.

  2. Computed attributes: If the desired key is not literally present as data on the entity of the current context then we can try to compute it from one (or many) other readers, those readers are usually maps (if your query is known to have a closed sets of attributes) or multimethods (if you want an extensible open sets of attributes), and they can be configured to handle the keys by doing some process/computation. There are 2 categories of these:

    1. Globals: if the computed attribute doesn’t depend on any data from the entity in the current context then it is a global computation and can be used independent of the current context.

    2. Derived attributes: Computing the attribute depends on some existing property of the current entity. Derivations are often relationship mappings, like navigating a join on :customer/address or :customer/friends. They may also be a transformation of real data from the contextual entity.

  3. Entity lookups: This is the Om.next default way to look for a specific entity on the graph using the notation [:customer/id 123] (where the first element names a unique identity key, and the second gives the unique id). Using such a literal entity lookup is a way of explicitly setting the context. It is essentially a "goto entity" for graph notation.

We will cover examples of all of these in this chapter. Pathom also includes Connect: an API defining the mechanism of Computed attributes. Many people will find that API to be the most direct path to a flexible parser for their system.

Use qualified (ideally namespaced) keywords. You should take great care to use unique names for your graph query attributes. The contextual behavior can lead to confusion very quickly, and it is made worse if you’re not sure which part of your query is even running. Also, software evolution will almost certainly lead you to use your queries with new APIs, and you will want to avoid naming collisions.

4.6. Some Basics

In this section we’ll develop a small parser that uses the primary elements of the core API. First, let’s talk about some of the basics a little more.

4.6.1. Reader Chains

It is very common for your parser to need more than one reader in order to process your queries. In these cases the :p/reader option to the parser can be a vector of readers. If a vector is given then they are tried in order until one of them resolves the read (or they all give up). Since nil is a valid possible value the a reader must return special values in order to indicate how the chain should behave.

  • It can return ::p/not-found to indicate the attribute was not found, and no further action should be taken.

  • It can return ::p/continue to indicate the attribute was not found, but the next reader (if available) should be tried.

4.6.2. The Map Reader

Not all things need to be computed. Very often the current context will already have attributes that were read during some prior step (for example, a computed attribute might have read an entire entity from the database and made it the current context). The map reader plugin is a plugin that has the following behavior:

  • If the attribute requested exists in the current parsing context (with any value, even nil), it returns it.

  • If the attribute is missing, it returns ::p/continue, which is an indication to move to the next reader in the chain.

  • If the attribute is present, it properly returns the value.

The map reader is also capable of resolving relations (if present in the context). For example, if there is a join in the query and a vector of data at that join key in the context, then it will attempt to fulfill the subquery of the join.

The map reader is almost always inserted into a reader chain because it is so common to read clumps of things from a database into the context and resolve them one by one as the query parsing proceeds.

4.6.3. Plugins

Pathom allows a parser to have a collection of plugins that modify its behavior. Plugins is a top-level option when creating the parser, and the value is a vector of plugins:

(def parser (p/parser {::p/plugins [...]}))

In this section we’ll be using a few plugins to make our lives easier.

4.6.4. The Environment Plugin

Typically the parsing environment will need to include things you create and inject every time you parse a query (e.g. a database connection) and some parser-related things (e.g. the reader) that might be the same all the time.

In the earlier example we created the parser and then explicitly supplied a reader to the environment every time we called it. In cases where there are specific things that you’d always like included in the environment we can instead use the plugin system to pre-set them for every parse.

So, in our prior example we had:

(def parser (p/parser {}))

and every call to the parser needed an explicit reader: (parser {::p/reader computed} [:hello])

The p/env-plugin is a parser plugin that automatically merges a map of configuration into the parsing environment every time the parser is called. Thus, our earlier example can be converted to:

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader computed})]}))

and now each call to the parser needs nothing in the env on each invocation: (parser {} [:hello]).

Providing an environment is such a common operation that there is a shortcut to set it up:

(def parser (p/parser {::p/env {::p/reader computed}}))

The ::p/env option to the parser tells it to install the env-plugin with the given configuration.

4.6.5. The Example

The example below sets up a more complex scenario where we’ll want to pass a database in the environment on every call (it might change over time), but we’ll place the reader logic in the environment plugin so we can only mention that part of the setup once.

Here’s a quick summary of the things we’ll be using for quick reference:

p/join

Satisfy a to-one join, and continue parsing.

p/join-seq

Satisfy a to-many join, and continue parsing

p/entity-attr!

Look up a (required) attribute from the current entity (context). Throws if missing.

p/map-reader

The map reader, as discussed previously.

Our example first starts with the expected preamble:

(ns pathom-docs.hello-entities
  (:require [com.wsscode.pathom.core :as p]))

Next we define some pretend database tables where the key is our pretend row ID, and the value is a pretend entity. Since we’re using keywords as IDs, you’ll also note that our "relations" are either simple keywords (to-one) or vectors of keywords (to-many):

; define some data of tv shows, as a simple map-based table
(def tv-shows
  {:rm  #:tv-show{:title         "Rick and Morty"
                  :character-ids [:rick :summer :morty]}
   :bcs #:tv-show{:title         "Better Call Saul"
                  :character-ids [:bcs]}
   :got #:tv-show{:title         "Game of Thrones"
                  :character-ids [:arya :ygritte]}})

; TV show characters, "joined" to tv-shows by "tv-show-id"
(def characters
  {:rick    #:character{:name "Rick Sanshes" :tv-show-id :rm}
   :summer  #:character{:name "Summer Smith" :tv-show-id :rm}
   :saul    #:character{:name "Saul Goodman" :tv-show-id :bcs}
   :arya    #:character{:name "Arya Stark" :tv-show-id :got}
   :morty   #:character{:name "Morty Smith" :tv-show-id :rm}
   :ygritte #:character{:name "Ygritte" :tv-show-id :got}})

thus, the :character/tv-show-id :got entry in the characters table is a pointer to the Game of Thrones (got) TV show in the TV shows table.

Next we’ll define a helper function that will help us look up things in these tables.

we’ll combine the tables above together into our parsing environment as :db in a later step, like this {:db (atom {:characters characters :tv-shows tv-shows})}. Our helpers assume this.
(defn characters-by-ids
  "This helper is given the parsing environment (where we'll pass in our database) and a list of character IDs.
   It returns the sequence of rows in the table that match those IDs."
  [{::keys [db] :as env} ids]
  (map (get @db :characters) ids))

and next a map-based reader for "resolving" a few things that are derived from either the context, or a more complex "query" of the database. Note that we’re parsing a graph query, so whenever we expect an attribute to be used as a join we must use the join helpers to tell the parser to continue processing the subquery of the join:

;; an example of a reader that doesn't need any context: This returns a random character from our database.
(def computed
  {:characters/random
   (fn [{::keys [db] :as env}]
     (let [character (rand-nth (-> @db :characters vals vec))]
       ; Use `join` to recursively parse the sub-query with the new to-one entity context
       (p/join character env)))

   ; another "globally accessible" bit of data that can be queried (independent of context)
   ; and returns a to-many result of some pre-chosen "leaing role" characters.
   :characters/main
   (fn [env]
     ; Process to-many joins with join-seq using a sequence as the new context:
     (p/join-seq env (characters-by-ids env [:rick :morty :saul :arya])))

   ; an example of a more complex computed relashionship:
   ; extract the tv-show according to the :character/tv-show-id
   ; on a character entity (current context)
   :character/tv-show
   (fn [{::keys [db] :as env}]
     ; the p/entity-attr! will try to get the :character/tv-show from current entity
     ; if it's not there it will make a query for it using the same parser. If
     ; it can't be got it will trigger an exception with the issue details, making
     ; easier to identify the problem
     (let [tv-show-id (p/entity-attr! env :character/tv-show-id)]
       (p/join (some-> @db :tv-shows (get tv-show-id)) env)))

   ; example of making a computed property, get the number of characters
   ; (the current context must be a tv-show)
   :tv-show/characters-count
   (fn [env]
     ; just give a count on members, and again, will raise exception if
     ; :tv-show/character-ids fails to be reached
     (count (p/entity-attr! env :tv-show/character-ids)))})

Finally we can define the parser itself. Note that we add in a p/map-reader to do the work of looking up any queried attributes that we’ve already placed in the environment (via join and join-seq):

(def parser
  ; This time we are using the env-plugin to initialize the environment, this is good
  ; to set the defaults for your parser to be called. Also, we are attaching the built-in
  ; reader map-reader on the game, so it will read the keys from the entity map. Check
  ; Entity page on wiki for more information.
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader computed]})]}))

Now we can call the parser any number of times with various queries. Note that this is where we’re injecting our simple map-based database into the environment:

; call the parser, create and send our atom database
(parser {::db (atom {:characters characters
                     :tv-shows   tv-shows})}
        [{:characters/main [:character/name {:character/tv-show [:tv-show/title
                                                                 :tv-show/characters-count]}]}
         ; feeling lucky today?
         {:characters/random [:character/name]}])
; =>
; #:characters{:main   [#:character{:name "Rick Sanshes", :tv-show #:tv-show{:name "Rick and Morty", :characters-count 3}}
;                       #:character{:name "Morty Smith", :tv-show #:tv-show{:name "Rick and Morty", :characters-count 3}}
;                       #:character{:name "Saul Goodman", :tv-show #:tv-show{:name "Better Call Saul", :characters-count 1}}
;                       #:character{:name "Arya Stark", :tv-show #:tv-show{:name "Game of Thrones", :characters-count 2}}],
;              :random #:character{:name "Saul Goodman"}}

4.7. Entity Lookups

Entity lookups are done by ident (as defined by Om Next and Fulcro). An ident is simply a vector whose first element is a keyword and second element is any value. The first element is thought of as the "table" or "kind" of thing, and the second as the ID of a specific one. For example [:character/id :rick] is an ident that "conceptually points to" the entity of type/table "character/id" with ID ":rick".

You typically add support for entity lookups using a multimethod that dispatches on the first element of an ident. The predefined value ::p/continue can be used by an entity lookup reader to indicate that it should ask the next reader to try to resolve the lookup.

We’ll be using one additional new function:

p/ident-value

Given the env, it will return the ID portion of the ident that is being resolved (current context).

The code below can be added to the prior example:

; databases and other prior code
...

;;;;;; Handle Entity Lookups ;;;;;;;

(defmulti entity p/entity-dispatch)

; default case returns ::p/continue to sign to pathom that
; this reader can't handle the given entry
(defmethod entity :default [_] ::p/continue)

(defmethod entity :character/id [{::keys [db] :as env}]
  ; from the key [:character/id :rick], p/ident-value will return :rick
  (let [id (p/ident-value env)]
    ; same thing as would find a record by id on your database
    ; we return ::p/continue to signal this reader wans't able to
    ; fetch it entity, so the parser can try the next one, more about this
    ; on Readers with page
    (p/join (get-in @db [:characters id] ::p/continue) env)))

; same thing for tv shows
(defmethod entity :tv-show/id [{::keys [db] :as env}]
  (let [id (p/ident-value env)]
    (p/join (get-in @db [:tv-shows id] ::p/continue) env)))

(def parser
  ; add our entity reader to our reader list
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader
                                                     computed
                                                     entity]})]}))

; testing our new queries
(parser {::db (atom {:characters characters
                     :tv-shows   tv-shows})}
        [[:character/id :arya] ; query for the entire entity
         {[:tv-show/id :rm] ; use the given tv show as the "context" for the join
          [:tv-show/title
           {:tv-show/characters [:character/name]}]}])
; =>
; {[:character/id :arya] #:character{:name "Arya Stark", :tv-show-id :got}
;  [:tv-show/id :rm]     #:tv-show{:title      "Rick and Morty"
;                                  :characters [#:character{:name "Rick Sanshes"}
;                                               #:character{:name "Summer Smith"}
;                                               #:character{:name "Morty Smith"}]}}

These building blocks enable quite a bit of query processing. As your graph grows larger it will make sense to use some additional tools to help split your parser into different pieces. In particular computed can be written with a dispatch mechanism instead of being represented as a map. See the section about dispatch helpers for more information.

Here is the complete code for the example:

(ns pathom-docs.hello-entities
  (:require [com.wsscode.pathom.core :as p]))

(def tv-shows
  {:rm  #:tv-show{:title         "Rick and Morty"
                  :character-ids [:rick :summer :morty]}
   :bcs #:tv-show{:title         "Better Call Saul"
                  :character-ids [:bcs]}
   :got #:tv-show{:title         "Game of Thrones"
                  :character-ids [:arya :ygritte]}})

(def characters
  {:rick    #:character{:name "Rick Sanshes" :tv-show-id :rm}
   :summer  #:character{:name "Summer Smith" :tv-show-id :rm}
   :saul    #:character{:name "Saul Goodman" :tv-show-id :bcs}
   :arya    #:character{:name "Arya Stark" :tv-show-id :got}
   :morty   #:character{:name "Morty Smith" :tv-show-id :rm}
   :ygritte #:character{:name "Ygritte" :tv-show-id :got}})

(defn characters-by-ids [{::keys [db]} ids]
  (map (get @db :characters) ids))

(def computed
  {:characters/random
   (fn [{::keys [db] :as env}]
     ; take a hand of the entity we want to be the current node
     (let [character (rand-nth (-> @db :characters vals vec))]
       ; to parse the sub-query with the entity we use the join function
       (p/join character env)))

   :characters/main
   (fn [env]
     ; since we decided to get the env in the characters-by-ids the argument
     ; passing is a brease
     (p/join-seq env (characters-by-ids env [:rick :morty :saul :arya])))

   :character/tv-show
   (fn [{::keys [db] :as env}]
     (let [tv-show-id (p/entity-attr! env :character/tv-show-id)]
       (p/join (some-> @db :tv-shows (get tv-show-id)) env)))

   :tv-show/characters
   (fn [env]
     (let [ids (p/entity-attr! env :tv-show/character-ids)]
       (p/join-seq env (characters-by-ids env ids))))

   :tv-show/characters-count
   (fn [env]
     (count (p/entity-attr! env :tv-show/character-ids)))})

(defmulti entity p/entity-dispatch)

(defmethod entity :default [_] ::p/continue)

(defmethod entity :character/id [{::keys [db] :as env}]
  (let [id (p/ident-value env)]
    (p/join (get-in @db [:characters id] ::p/continue) env)))

(defmethod entity :tv-show/id [{::keys [db] :as env}]
  (let [id (p/ident-value env)]
    (p/join (get-in @db [:tv-shows id] ::p/continue) env)))

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader
                                                     computed
                                                     entity]})]}))

(parser {::db (atom {:characters characters
                     :tv-shows   tv-shows})}
        [[:character/id :arya]
         {[:tv-show/id :rm]
          [:tv-show/title
           {:tv-show/characters [:character/name]}]}])
; =>
; {[:character/id :arya] #:character{:name "Arya Stark", :tv-show-id :got}
;  [:tv-show/id :rm]     #:tv-show{:title      "Rick and Morty"
;                                  :characters [#:character{:name "Rick Sanshes"}
;                                               #:character{:name "Summer Smith"}
;                                               #:character{:name "Morty Smith"}]}}

5. Pathom Core Engine

5.1. Readers

A reader is a function that will process a single entry from the query. For example, given the following query: [:name :age]. If you ask an om.next parser to read this the reader function will be called twice; once for :name and another one for :age. Note that in the case of joins, the parser will only be called for the join entry, but not for it’s children (not automatically), for example: given the query [:name :age {:parent [:name :gender]}]. The reader function will be called 3 times now, one for :name, one for :age and one for :parent, when reading :parent, your reader code is responsible for checking that it has a children query, and do a recursive call (or anything else you want to do to handle this join). During this documentation, we are going to see many ways to implement those readers.

Please note the following differences between om.next readers and pathom readers: In om.next a parse read functions has the following signature: (fn [env dispatch-key params]). In pathom we use a smaller version instead, which is: (fn [env]). The env already contains the dispatch-key and params, so there is no loss of information.

(get-in env [:ast :dispatch-key]) ; => dispatch-key
(get-in env [:ast :params]) ; => params

Also, in om.next you need to return the value wrapped in {:value "your-content"}. In pathom this wrapping is done automatically for you: just return the final value.

Readers can be 1-arity function, maps, or vectors. See Map dispatcher and Vector dispacher for information on those respectively.

Here is a formal Clojure Spec definiton for a pathom reader:

(s/def ::reader-map (s/map-of keyword? ::reader))
(s/def ::reader-seq (s/coll-of ::reader :kind vector?))
(s/def ::reader-fn (s/fspec :args (s/cat :env ::env)
                            :ret any?))

(s/def ::reader
  (s/or :fn ::reader-fn
        :map ::reader-map
        :list ::reader-seq))

5.1.1. Functions as Readers

These are quite simply a function that receive the env and resolve the read. More than one reader can exist in a chain, and the special return value ::p/continue allows a reader to indicate it cannot resolve the given property (to continue processing the chain). Returning any value (including nil) you’ve resolved the property to that value.

(ns pathom-docs.fn-dispatch
  (:require [com.wsscode.pathom.core :as p]))

(defn read-value [{:keys [ast]}]
  (let [key (get ast :dispatch-key)]
    (case key
      :name "Saul"
      :family "Goodman"
      ; good pratice: return ::p/continue when your reader is unable
      ; to handle the request
      ::p/continue)))

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader read-value})]}))

(parser {} [:name :family])
; => {:name "Saul" :family "Goodman"}

5.1.2. Maps as Readers

Since it is very common to want to resolve queries from a fixed set of possibilities we support defining a map as a reader. This is really just a "dispatch table" to functions that will receive env. We can re-write the previous example as:

(ns pathom-docs.reader-map-dispatch
  (:require [com.wsscode.pathom.core :as p]))

(def user-reader
  {:name   (fn [_] "Saul")
   :family (fn [_] "Goodman")})

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader user-reader})]}))

(parser {} [:name :family])
; => {:name "Saul" :family "Goodman"}
The built-in Map Reader will return ::p/continue if the map it is looking in does not contain the key for the attribute being resolved. This allows it to be safely used in a vector of readers.

5.1.3. Vectors of Readers [aka composed readers]

Using a vector for a reader is how you define a chain of readers. This allows you to define readers that serve a particular purpose. For example, some library author might want to supply readers to compose into your parser, or you might have different modules of database-specific readers that you’d like to keep separate.

When pathom is trying to resolve a given attribute (say :person/name) in some context (say against the "Sam" entity) it will start at the beginning of the reader chain. The first reader will be asked to resolve the attribute. If the reader can handle the value then it will be returned and no other readers will be consulted. If it instead returns the special value ::p/continue it is signalling that it could not resolve it (map readers do this if the attribute key is not in their map). When this happens the next reader in the chain will be tried.

(ns pathom-docs.reader-vector-dispatch
  (:require [com.wsscode.pathom.core :as p]))

; a map dispatcher for the :name key
(def name-reader
  {:name   (fn [_] "Saul")})

; a map dispatcher for the :family key
(def family-reader
  {:family (fn [_] "Goodman")})

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader [name-reader family-reader]})]}))

(parser {} [:name :family :other])
; => {:name "Saul", :family "Goodman", :other :com.wsscode.pathom.core/not-found}

If no reader in the chain returns a value (all readers reeturn ::p/continue), then ::p/not-found will be returned.

When you write your readers you should always remember to return ::p/continue when you can’t handle a given key. This way your reader will play nice in composition scenarios.

5.1.4. Dynamic Readers

Recursive calls are widespread during parsing, and Om.next makes it even easier by providing the current parser as part of the environment. The problem is that if you just call the same parser recursively then there is no chance to change how the reading process operates.

In order to improve this situation pathom makes the :parser and :reader part of the environment, allowing you to replace it when doing a recursive parser call:

(ns pathom-dynamic-reader
  (:require [com.wsscode.pathom.core :as p]))

(defn user-reader [{:keys [ast]}]
  (let [key (get ast :dispatch-key)]
    (case key
      :name "Saul"
      :family "Goodman")))

(defn root-reader [{:keys [ast query parser] :as env}]
  (let [key (get ast :dispatch-key)]
    (case key
      :current-user (parser (assoc env ::p/reader user-reader) query))))

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader root-reader})]}))

(parser {} [{:current-user [:name :family]}])
; => {:current-user {:name "Saul" :family "Goodman"}}
Replacing the reader when parsing is rarely needed in practice. The functional dispatch and vector of readers cover most common cases; however, replacing the reader could be useful in a scenario such as code splitting where the readers were not all available at the time of initial parser construction.

5.2. Entities

An entity to pathom is the graph node that is tracked as the current context, and from which information (attributes and graph edges to other entities) can be derived. The current entity needs to be "map-like": It should work with all normal map-related functions like get, contains?, etc.

As Pathom parses the query it tracks the current entity in the environment at key ::p/entity. This makes it easier to write more reusable and flexible readers as we’ll see later.

5.2.1. Using p/entity

The p/entity function exists as a convenience for pulling the current entity from the parsing environment:

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

Note that the code above is a partial implementation of the map-dispatcher.

The map-reader just has the additional ability to understand how to walk a map that has a tree shape that already "fits" our query:

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

Now that you understand where the entity context is tracked I encourage you to check the p/map-reader implementation. It’s not very long and will give you a better understanding of all of the concepts covered so far.

5.2.2. Understanding Joins

The other significant task when processing a graph query is walking a graph edge to another entity (or entities) when we find a join.

The subquery for a join is in the :query of the environment. Essentially it is a recursive step where we run the parser on the subquery while replacing the "current entity":

(defn join [entity {:keys [parser query] :as env}]
  (parser (assoc env ::p/entity entity) query))

The real pathom implementation handles some additional scenarios: like the empty sub-query case (it returns the full entity), the special * query (so you can combine the whole entity + extra computed attributes), and union queries.

The following example shows how to use p/join to "invent" a relation that can then be queried:

(ns com.wsscode.pathom-docs.using-entity-map-reader
  (:require [com.wsscode.pathom.core :as p]))

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

(def char-name->voice
  "Relational information representing edges from character names to actors"
  {"Rick"   #:actor{:name "Justin Roiland" :nationality "US"}
   "Morty"  #:actor{:name "Justin Roiland" :nationality "US"}
   "Summer" #:actor{:name "Spencer Grammer" :nationality "US"}})

(def computed
  {:character/voice ; support an invented join attribute
   (fn [env]
     (let [{:character/keys [name]} (p/entity env)
           voice (get char-name->voice name)]
       (p/join voice env)))})

(def parser
  ; process with map-reader first, then try with computed
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader computed]})]}))

(parser {::p/entity rick} ; start with rick (as current entity)
        '[:character/name
          {:character/voice [:actor/name]}
          {:character/family [* :character/voice]}])

There are three different scenarios demonstrated in the above query:

  1. Using the invented join property in a normal join. This allows for a subquery that constrains the data returned (from the actor in this case).

  2. Using the * in a query, which returns all "known" attributes of the "current contextual" entity.

  3. Using an additional (non-joined) :character/voice with * "adds in" that additional information. When a property is queried for that is processed via p/join then the entire entity will be returned even though there is no subquery.

5.2.3. Dependent Attributes

When computing attributes it is possible that you might need some other attribute for the current context that is also computed. You could hard-code a solution, but that would create all sorts of static code problems that could be difficult to manage as your code evolves: changes to the readers, for example, could easily break it and lead to difficult bugs.

Instead, it is important that readers be able to resolve attributes they need from the "current context" in an abstract manner (i.e. the same way that they query itself is being resolved). The p/entity function has an additional arity for handling this exact case. You pass it a list of attributes that should be "made available" on the current entity, and it will use the parser to ensure that they are there (if possible):

(let [e (p/entity env [:x])]
   ; e now has :x on it if possible, even if it is computed elsewhere
   ...)

The following example shows this in context:

(ns pathom-docs.entity-attribute-dependency
  (:require [com.wsscode.pathom.core :as p]))

(def computed
  {:greet
   (fn [env]
     (let [{:character/keys [name]} (p/entity env)]
       (str "Hello " name "!")))

   :invite
   (fn [env]
     ; requires the computed property `:greet`, which might not have been computed into the current context yet.
     (let [{:keys [greet]} (p/entity env [:greet])]
       (str greet " Come to visit us in Neverland!")))})

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader
                                                     computed]})]}))

(parser {::p/entity #:character{:name "Mary"}}
        [:invite])
; => {:invite "Hello Mary! Come to visit us in Neverland!"}

There is a variant p/entity! that raises an error if your desired attributes are not found. It’s recommended to use the enforced version if you need the given attributes, as it will give your user a better error message.

(ns pathom-docs.entity-attribute-enforce
  (:require [com.wsscode.pathom.core :as p]))

(def computed
  {:greet
   (fn [env]
     ; enfore the character/name to be present, otherwise raises error, try removing
     ; the attribute from the entity and see what happens
     (let [name (p/entity-attr! env :character/name)]
       (str "Hello " name "!")))

   :invite
   (fn [env]
     ; now we are enforcing the attribute to be available, otherwise raise an error
     ; try changing the :greet to :greete and run the file, you will see the error
     (let [greet (p/entity-attr! env :greet)]
       (str greet " Come to visit us in Neverland!")))})

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader
                                                     computed]})]}))

(parser {::p/entity #:character{:name "Mary"}}
        [:invite])
; => {:invite "Hello Mary! Come to visit us in Neverland!"}

If the parse fails on an enforced attribute you will get an exception. For example, if the current entity were #:character{:nam "Mary"} we’d see:

CompilerException clojure.lang.ExceptionInfo: Entity attributes #{:character/name} could not be realized #:com.wsscode.pathom.core{:entity #:character{:nam "Mary"}, :path [:invite :greet], :missing-attributes #{:character/name}}
If computed attributes require IO or intense computation you should consider adding caching to improve parsing performance. Remember that a given query might traverse the same node more than once! Imagine a query that asks for your friends and co-workers. When there is this kind of overlap the same computational code may run more than once. See Request Caching for more details.

5.2.4. Atom entities

As you move from node to node, you can choose to wrap the new contextual entity in an atom. This can be used as a narrow kind of caching mechanism that allows for a reader to add information into the current entity as it computes it, but which is valid for only the processing of the current entity (is lost as soon as the next join is followed). Therefore, this won’t help with the overhead of re-visiting the same entity more than once when processing different parts of the same query.

The built-in function p/entity always returns a Clojure map, if the entity is an atom it will deref it automatically.

Here is an example using an entity atom:

link:../src-docs/com/wsscode/pathom/book/entities/atom_entities.cljc[role=include]

5.2.5. Union queries

Union queries allow us to handle edges that lead to heterogeneous nodes. For example a to-many relation for media that could result in a book or movie. Following such an edge requires that we have a different subquery depending on what we actually find in the database.

Here is an example where we want to use a query that will search to find 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]}}])

Of course, unions need to have a way to determine which path to go based on the entity at hand. In the example above we used 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 (that takes env and returns the correct key (e.g. :book) to use for the union query).

If you want ::p/union-path to be more contextual you can of course set it in the env 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 is more common since the *method* of determining type will vary for
     ; different queries and data.
     (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.

5.3. Error handling

By default, pathom parser will stop if some exception occurs during the parsing process. This is often undesirable if some node fails you still can return the other ones that succeed. You can use the error-handler-plugin. This plugin will wrap each read call with a try-catch block, and in case an error occurs, a value of ::p/reader-error will be placed in that node, while details of it will go in a separate tree, but at the same path. Better an example to demonstrate:

(ns pathom-docs.error-handling
  (:require [com.wsscode.pathom.core :as p]))

(def computed
  ; create a handle key that will trigger an error when called
  {:trigger-error
   (fn [_]
     (throw (ex-info "Error triggered" {:foo "bar"})))})

; a reader that just flows, until it reaches a leaf
(defn flow-reader [{:keys [query] :as env}]
  (if query
    (p/join env)
    :leaf))

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [computed flow-reader]})
                          ; add the error handler plugin
                          p/error-handler-plugin]}))

(parser {} [{:go [:key {:nest [:trigger-error :other]}
                  :trigger-error]}])
; =>
; {:go {:key :leaf
;       :nest {:trigger-error :com.wsscode.pathom.core/reader-error
;              :other :leaf}
;       :trigger-error :com.wsscode.pathom.core/reader-error}
;  :com.wsscode.pathom.core/errors {[:go :nest :trigger-error] "class clojure.lang.ExceptionInfo: Error triggered - {:foo \"bar\"}"
;                                   [:go :trigger-error] "class clojure.lang.ExceptionInfo: Error triggered - {:foo \"bar\"}"}}

As you can see, when an error occurs, the key ::p/errors will be added to the returned map, containing the detailed error message indexed by the error path. You can customize how the error is exported in this map by setting the key ::p/process-error in your environment:

(ns pathom-docs.error-handling-process
  (:require [com.wsscode.pathom.core :as p]))

(def computed
  ; create a handle key that will trigger an error when called
  {:trigger-error
   (fn [_]
     (throw (ex-info "Error triggered" {:foo "bar"})))})

; a reader that just flows, until it reaches a leaf
(defn flow-reader [{:keys [query] :as env}]
  (if query
    (p/join env)
    :leaf))

; our error processing function
(defn process-error [env err]
  ; if you use some error reporting service, this is a good place
  ; to trigger a call to then, here you have the error and the full
  ; environment of when it ocurred, so you might want to some extra
  ; information like the query and the current path on it so you can
  ; replay it for debugging

  ; we are going to simply return the error message from the error
  ; if you want to return the same thing as the default, use the
  ; function (p/error-str err)
  (.getMessage err))

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [computed flow-reader]
                                         ; add the error processing to the environment
                                         ::p/process-error process-error})
                          ; add the error handler plugin
                          p/error-handler-plugin]}))

(parser {} [{:go [:key {:nest [:trigger-error :other]}
                  :trigger-error]}])
; =>
; {:go {:key :leaf
;       :nest {:trigger-error :com.wsscode.pathom.core/reader-error
;              :other :leaf}
;       :trigger-error :com.wsscode.pathom.core/reader-error}
;  :com.wsscode.pathom.core/errors {[:go :nest :trigger-error] "Error triggered"
;                                   [:go :trigger-error]       "Error triggered"}}

5.3.1. Fail fast

Having each node being caught is great for the UI, but not so much for testing. During testing you probably prefer the parser to blow up as fast as possible so you don’t accumulate a bunch of errors that get impossible to read. Having to create a different parser to remove the error-handler-plugin can be annoying, so there is an option to solve that. Send the key ::p/fail-fast? as true in the environment, and the try/catch will not be done, making it fail as soon as an exception fires, for example, using our previous parser:

(parser {::p/fail-fast? true}
        [{:go [:key {:nest [:trigger-error :other]}
               :trigger-error]}])
; => CompilerException clojure.lang.ExceptionInfo: Error triggered {:foo "bar"}, ...

5.3.2. Raising errors

The default error output format (in a separated tree) is very convenient for direct API calls, because they leave a clean output on the data part. But if you want to expose those errors on the UI, pulling then out of the separated tree can be a bit of a pain. To help with that there is a p/raise-errors helper, this will lift the errors so they are present at the same level of the error entry. Let’s take our last error output example and process it with p/raise-errors

(p/raise-errors {:go {:key :leaf
                      :nest {:trigger-error :com.wsscode.pathom.core/reader-error
                             :other :leaf}
                      :trigger-error :com.wsscode.pathom.core/reader-error}
                 :com.wsscode.pathom.core/errors {[:go :nest :trigger-error] "Error triggered"
                                                  [:go :trigger-error] "Error triggered"}})

; outputs:

{:go {:key :leaf
      :nest {:trigger-error :com.wsscode.pathom.core/reader-error
             :other :leaf
             :com.wsscode.pathom.core/errors {:trigger-error "Error triggered"}}
      :trigger-error :com.wsscode.pathom.core/reader-error
      :com.wsscode.pathom.core/errors {:trigger-error "Error triggered"}}}

Notice that we don’t have the root ::p/errors anymore, instead it is placed at the same level of the error attribute. So the path [::p/errors [:go :nest :trigger-error]] turns into [:go :nest ::p/errors :trigger-error]. This makes very easy to pull the error on the client-side.

5.4. Dispatch helpers

Using multi-methods is a good way to make open readers, pathom provides helpers for two common dispatch strategies: key-dispatch and entity-dispatch. Here is a pattern that I often use on parsers:

(ns pathom-docs.dispatch-helpers
  (:require [com.wsscode.pathom.core :as p]))

(def cities
  {"Recife"    {:city/name "Recife" :city/country "Brazil"}
   "São Paulo" {:city/name "São Paulo" :city/country "Brazil"}})

(def city->neighbors
  {"Recife" [{:neighbor/name "Boa Viagem"}
             {:neighbor/name "Piedade"}
             {:neighbor/name "Casa Amarela"}]})

; this will dispatch according to the ast dispatch-key
(defmulti computed p/key-dispatch)

; use virtual attributes to handle data not present on the maps, like computed attributes, relationships, and globals
(defmethod computed :city/neighbors [env]
  (let [name (p/entity-attr! env :city/name)]
    (p/join-seq env (city->neighbors name))))

; an example of global, same as before but without any dependency on the entity
(defmethod computed :city/all [env]
  (p/join-seq env (vals cities)))

; remember to return ::p/continue by default so non-handled cases can flow
(defmethod computed :default [_] ::p/continue)

; just to make easy to re-use, our base entity reader consists of a map reader + virtual attributes
(def entity-reader [p/map-reader computed])

; dispatch for entity keys, eg: [:user/by-id 123]
(defmulti entity-lookup p/entity-dispatch)

(defmethod entity-lookup :city/by-name [env]
  ; the ident-value helper extracts the value part from the ident, as "Recife" in [:city/by-name "Recife"]
  (let [city (get cities (p/ident-value env))]
    (p/join city env)))

(defmethod entity-lookup :default [_] ::p/continue)

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader computed entity-lookup]})]}))

(parser {} [{:city/all [:city/name]}
            {[:city/by-name "Recife"] [:city/neighbors]}])
; =>
;{:city/all [#:city{:name "Recife"} #:city{:name "São Paulo"}]
; [:city/by-name "Recife"] #:city{:neighbors [#:neighbor{:name "Boa Viagem"}
;                                             #:neighbor{:name "Piedade"}
;                                             #:neighbor{:name "Casa Amarela"}]}}

5.5. Mutations

To handle mutations, you can send the :mutate param to the parser.

(ns com.wsscode.pathom.book.mutation
  (:require [com.wsscode.pathom.core :as p]
            [fulcro.client.primitives :as fp]))

(defmulti my-mutate fp/dispatch)

(defmethod my-mutate `do-operation [{:keys [state]} _ params]
  (swap! state update :history conj {:op :operation :params params}))

(def parser (p/parser {:mutate my-mutate}))

(comment
  (let [state (atom {:history []})]
    (parser {:state state} [`(do-operation {:foo "bar"})
                            `(do-operation {:buz "baz"})])
    @state)
  ; => {:history [{:op :operation, :params {:foo "bar"}}
  ;               {:op :operation, :params {:buz "baz"}}]}
  )

5.6. Request Caching

As your queries grow, there are more and more optimizations that you can do avoid unnecessary IO or heavy computations. Here we are going to talk about a request cache, which is a fancy name for an atom that is initialized on every query and stays on the environment so you can share the cache across nodes. Let’s see how we can use that to speed up our query processing:

(ns pathom-docs.request-cache
  (:require [com.wsscode.pathom.core :as p]))

(defn my-expensive-operation [env]
  ; the cache key can be anything; if we were had an extra
  ; variable here, like some id, a good cache key would be
  ; like: [::my-expensive-operation id]
  (p/cached env :my-key
    ; we are going to send an atom with an int so that we can count
    ; how many times this was called
    (let [counter (:counter env)]
      ; a secondary sign if cache is working, let's make a delay
      (Thread/sleep 1000)
      ; increment and return
      (swap! counter inc))))

(def computed
  {:cached my-expensive-operation})

; a reader that just flows, until it reaches a leaf
(defn flow-reader [{:keys [query] :as env}]
  (if query
    (p/join env)
    :leaf))

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [computed
                                                     flow-reader]})
                          ; add the request cache plugin for cache initialization
                          p/request-cache-plugin]}))

(time
  (parser {:counter (atom 0)}
          [:x :y :cached
           {:z [:foo {:bar [:cached]} :cached]}]))
; "Elapsed time: 1006.760165 msecs"
; =>
; {:x      :leaf
;  :y      :leaf
;  :cached 1
;  :z      {:foo    :leaf
;           :bar    {:cached 1}
;           :cached 1}}

Remember this cache is per request, so after a full query gets finished, the atom is discarded. If you want to make a cache that’s more durable (that retains information across requests), check the [[Plugins|Plugins]] documentation for more information on how to do that.

5.7. Plugins

Plugins set code that wraps some of pathom operations, a plugin is a map where you bind keys from event names to functions. They work on wrap fashion, kind like ring wrappers. Here is what a plugin looks like:

(ns pathom-docs.plugin-example
  (:require [com.wsscode.pathom.core :as p]))

(def my-plugin
  ; the ::p/wrap-parser entry point wraps the entire parser,
  ; this means it wraps the operation that runs once on each
  ; query that runs with the parser
  {::p/wrap-parser
   (fn [parser]
     ; here you can initialize stuff that runs only once per
     ; parser, like a durable cache across requests
     (fn [env tx]
       ; here you could initialize per-request items, things
       ; that needs to be set up once per query as we do on
       ; request cache, or the error atom to accumulate errors

       ; in this case, we are doing nothing, just calling the
       ; previous parser, a pass-through wrapper if you may
       (parser env tx)))

   ; this wraps the read function, meaning it will run once for
   ; each recursive parser call that happens during your query
   ::p/wrap-read
   (fn [reader]
     (fn [env]
       ; here you can wrap the parse read, in pathom we use this
       ; on the error handler to do the try/catch per node, also
       ; the profiler use this point to calculate the time spent
       ; on a given node

       ; this is also a good point to inject custom read keys if
       ; you need to, the profile plugin, for example, can capture
       ; the key ::p.profile/profile and export the current profile
       ; information
       (reader env)))})

The plugin engine replaces the old process-reader in a much more powerful way. If you want to check a real example look for the source for the built-in plugins, they are quite small and yet powerful tools (grep for -plugin on the repository to find all of them).

5.7.1. Shard switch

For a more practical example, let’s say we are routing in a micro-service architecture and our parser needs to be shard-aware. Let’s write a plugin that anytime it sees a :shard param on a query; and it will update the :shard attribute on the environment and send it down, providing that shard information for any node downstream.

(ns pathom-docs.plugin-shard
  (:require [com.wsscode.pathom.core :as p]))

; a reader that just flows, until it reaches a leaf
(defn flow-reader [{:keys [query] :as env}]
  (if query
    (p/join env)
    :leaf))

(def shard-reader
  ; Clojure neat tricks, let's just fetch the shard
  ; from the environment when :current-shard is asked
  {:current-shard :shard})

(def shard-plugin
  {::p/wrap-read
   (fn [reader]
     (fn [env]
       ; try to get a new shard from the query params
       (let [new-shard (get-in env [:ast :params :shard])]
         (reader (cond-> env new-shard (assoc :shard new-shard))))))})

(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [shard-reader flow-reader]})
                          ; use our shard plugin
                          shard-plugin]}))

(parser {:shard "global"}
        '[:a :b :current-shard
          {(:go-s1 {:shard "s1"})
           ; notice it flows down
           [:x :current-shard {:y [:current-shard]}]}
          :c
          {(:go-s2 {:shard "s2"})
           [:current-shard
            ; we can override at any point
            {(:now-s3 {:shard "s3"})
             [:current-shard]}]}])
; =>
; {:a             :leaf
;  :b             :leaf
;  :current-shard "global"
;  :go-s1         {:x :leaf :current-shard "s1" :y {:current-shard "s1"}}
;  :c             :leaf
;  :go-s2         {:current-shard "s2" :now-s3 {:current-shard "s3"}}}

5.9. Placeholders

There is one issue that some people stumbled upon while using Om.next; the problem happens when you need to display two or more different views of the same item as siblings (regarding query arrangement, not necessarily DOM siblings), how do you make this query?

For example, let’s say you have two different components to display a user profile, one that shows just the username, and another one with its photo.

(om/defui ^:once UserTextView
  static om/IQuery
  (query [_] [:user/name]))

(om/defui ^:once UserImageView
  static om/IQuery
  (query [_] [:user/photo-url]))

(om/defui ^:once UserViewsCompare
  static om/IQuery
  ;; We want to query for both, what we place here?
  (query [_] [{:app/current-user [???]}]))

You might be tempted to concat the queries, and in case you don’t have to nest like we do here, that may even look like it’s working, but let me break this illusion for you; because it’s not. When you use om/get-query it’s not just the query that’s returned; it also contains meta-data telling from which component that query came from.

This information is important, om uses to index your structure and enables incremental updates. When you concat the queries, you lose this, and as a consequence, when you try to run a mutation later that touches those items you will have a “No queries exist at the intersection of component path” thrown in your face.

[This problem is still in discussion on the om repository](https://github.com/omcljs/om/issues/823). So far the best way I know to handle this is to use placeholder nodes, so let’s learn how to manage those cases properly.

What we need is to be able to branch out the different queries, this is my suggestion on how to write the UserViewsCompare query:

(om/defui ^:once UserViewsCompare
  static om/IQuery
  ;; By having extra possible branches we keep the path information working
  (query [_] [{:app/current-user [{:ph/text-view (om/get-query UserTextView)}
                                  {:ph/image-view (om/get-query UserImageView)}]}]))

The trick is to create a convention about placeholder nodes, in this case, we choose the namespace ph to represent “placeholder nodes”, so when the query asks for :ph/something we should just do a recursive call, but staying at the same logical position in terms of parsing, as if we had stayed on the same node.

You can use the p/placeholder-reader to implement this pattern on your parser:

(ns pathom-docs.placeholder
  (:require [com.wsscode.pathom.core :as p]))

(def user
  {:user/name      "Walter White"
   :user/photo-url "http://retalhoclub.com.br/wp-content/uploads/2016/07/1-3.jpg"})

(def computed
  {:app/current-user
   (fn [env]
     (p/join user env))})

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader [p/map-reader
                                                               computed
                                                               ; placeholder reader
                                                               (p/placeholder-reader "ph")]})]}))

(parser {} [{:app/current-user [{:ph/text-view [:user/name]}
                                {:ph/image-view [:user/photo-url]}]}])
; #:app{:current-user #:ph{:text-view #:user{:name "Walter White"},
;                          :image-view #:user{:photo-url "http://retalhoclub.com.br/wp-content/uploads/2016/07/1-3.jpg"}}}

5.10. Profiling

It’s good to know how your queries are performing, and breaking it down by nodes is an excellent level to reason about how your queries are doing. Pathom provides a plugin to make this measurement easy to do:

(ns pathom-docs.profile
  (:require [com.wsscode.pathom.core :as p]
            [com.wsscode.pathom.profile :as p.profile]))

(def computed
  ; to demo delays, this property will take some time
  {:expensive (fn [{:keys [query] :as env}]
                (Thread/sleep 300)
                (if query
                  (p/join env)
                  :done))})

(defn flow-reader [{:keys [query] :as env}]
  (if query
    (p/join env)
    :leaf))

; starting the parser as usual
(def parser
  (p/parser {::p/plugins [(p/env-plugin {::p/reader [computed flow-reader]})
                          ; include the profile plugin
                          p.profile/profile-plugin]}))

(parser {}
        ; run the things
        [:a :b {:expensive [:c :d {:e [:expensive]}]}
         ; profile plugin provide this key, when you ask for it you get the
         ; information, be sure to request this as the last item on your query
         ::p.profile/profile])
; =>
; {:a                  :leaf
;  :b                  :leaf
;  :expensive          {:c :leaf
;                       :d :leaf
;                       :e {:expensive :done}}
;  ::p.profile/profile {:a         0
;                       :b         0
;                       :expensive {:c               1
;                                   :d               0
;                                   :e               {:expensive 304
;                                                     ::p.profile/self 304}
;                                   ::p.profile/self 611}}}

Looking at the profile results, you see the query values, and at the edges is the ms time taken to process that node. When the node has children, a ::p.profile/self indicates the time for the node itself (including children).

If you like to print a flame-graph of this output, you can use some d3 libraries on the web, I recommend the [d3 flame graph from spierman](https://github.com/spiermar/d3-flame-graph). Pathom has a function to convert the profile data to the format accepted by that library:

(-> (parser {}
            ; let's add more things this time
            [:a {:b [:g {:expensive [:f]}]}
             {:expensive [:c :d {:e [:expensive]}]}
             ::p.profile/profile])
    ; get the profile
    ::p.profile/profile
    ; generate the name/value/children format
    p.profile/profile->nvc)
; =>
; {:name     "Root"
;  :value    910
;  :children [{:name ":a" :value 0}
;             {:name     ":b"
;              :value    305
;              :children [{:name ":g" :value 0} {:name ":expensive" :value 304 :children [{:name ":f" :value 1}]}]}
;             {:name     ":expensive"
;              :value    605
;              :children [{:name ":c" :value 0}
;                         {:name ":d" :value 1}
;                         {:name ":e" :value 301 :children [{:name ":expensive" :value 300}]}]}]}

And then use that data to generate the flame graph:

5.11. Path tracking

As you go deep in your parser pathom track record of the current path taken, it’s available at ::p/path at any time. It’s a vector containing the current path from the root, the current main use for it is regarding error reporting and profiling.

(ns pathom-docs.path-tracking
  (:require [com.wsscode.pathom.core :as p]))

(def where-i-am-reader
  {:where-am-i (fn [{::p/keys [path]}] path)})

; a reader that just flows, until it reaches a leaf
(defn flow-reader [{:keys [query] :as env}]
  (if query
    (p/join env)
    :leaf))

(def parser (p/parser {::p/plugins [(p/env-plugin {::p/reader [where-i-am-reader
                                                               flow-reader]})]}))

(parser {} [{:hello [:some {:friend [:place :where-am-i]}]}])
;=>
;{:hello {:some   :leaf
;         :friend {:place      :leaf
;                  :where-am-i [:hello :friend :where-am-i]}}}

5.12. Query Specs

Pathom providers a full specification of the query syntax, in spec format.

5.13. Async parsing

If you want to write parsers to run in Javascript environments, then async operations are the norm. The async parser is a version of the parser were you can return core async channels from the readers instead of raw values. This allows for the creation of parsers that do network requests or any other async operation. The async parser is still semantically a serial parser, and it will have the same flow characteristics of the regular parser (the order or resolution is preserved).

To write an async parser we use the p/async-parser function. Here is an example:

link:../src-docs/com/wsscode/pathom/book/async/intro.cljs[role=include]

Try the example:

[:foo :async-info]

The core plugins work normally with the async parser, so error and profiling will work as expected.

5.13.1. Error propagation

When an exception occurs inside a core async channel the error is triggered as part of the channel exception handler. That doesn’t compose very well, and for the parser needs it’s better if we have something more like the async/await pattern used on JS environments. Pathom provides some macros to help making this a simple thing, instead of using go and <!, use the go-catch and <? macros, as in the following example:

link:../src-docs/com/wsscode/pathom/book/async/error_propagation.cljs[role=include]
[:foo :async-info :async-error :com.wsscode.pathom.profile/profile]

Use com.wsscode.common.async-clj for Clojure and com.wsscode.common.async-cljs for ClojureScript. If you writing a cljc file, use the following:

[#?(:clj  com.wsscode.common.async-clj
    :cljs com.wsscode.common.async-cljs)
 :refer [go-catch <?]]

5.13.2. JS Promises

In JS world most of the current async responses comes as promises, you can use the <!p macro to read from promises inside go blocks as if they were channels. Example:

link:../src-docs/com/wsscode/pathom/book/async/js_promises.cljs[role=include]
[:dog.ceo/random-dog-url]

6. GraphQL Integration

Pathom provides a collection of utilities to integrate with GraphQL. There are two main ways in which to use GraphQL:

  1. Simple: Use utilities to convert queries/mutations to GraphQL, and parse the responses. This gives you a quick and easy interface to existing GraphQL APIs, but is not extensible.

  2. Advanced: Integrate with Connect. This method pulls the GraphQL schema into Connect indexes with various benefits: Tools give better support (e.g. query autocompletion within Fulcro Inspect), and you can add your own client-side resolvers that can derive new shapes/data for the API, making it possible to shape the external API to your local UI whims.

In both cases Pathom includes implementations of Fulcro Remotes, so you can easily drop GraphQL support into a Fulcro application as a remote!

This chapter assumes you’re familiar with Pathom’s async support.

The namespaces concerned are:

    [com.wsscode.pathom.graphql :as pg]
    [com.wsscode.pathom.connect.graphql :as pcg]
    [com.wsscode.pathom.fulcro.network :as pfn]

6.1. Simple GraphQL

There is a Fulcro Remote in pfn/graphql-network that allows you to easily add plain GraphQL support to a Fulcro client like so:

(fulcro/new-fulcro-client
    :networking
    {:remote
     (pfn/graphql-network
       {::pfn/url (str "https://api.github.com/graphql?access_token=" token)})})

The queries from components have the following rules:

  1. You can use any namespace on the query keywords.

  2. The name portion of a keyword can be hyphenated instead of camel-case

    1. Hyphenated names will automatically be converted to the standard GraphQL camel case.

Mutations on a Simple GraphQL remote have the following rules:

  1. Mutations can have any namespace. The GraphQL conversion will elide the namespace.

6.1.1. Simple GraphQL Example

To demonstrate how easy it is to get a simple application going against an external GraphQL API we’ll build a simple TODO app. We’ve already gone to graph.cool, and created a GraphQL schema at https://www.graph.cool/ (a back-end as a service provider). You can play with the API by entering queries and mutations via their interface to our endpoint at https://api.graph.cool/simple/v1/cjjkw3slu0ui40186ml4jocgk.

For example, entering this query into the left pane:

query {
  allTodoItems {id, title, completed}
}

should give you something like this (people play with this, so yours will be different):

{
  "data": {
    "allTodoItems": [
      {
        "id": "cjjkw7yws06el0135q5sf372s",
        "title": "Write docs on workspaces",
        "completed": true
      }]
  }
}

So, you can see we have a root query that we can run to get all todo items, and each one has an id and title. So, we can write a simple Fulcro tree of components for that query:

(defsc TodoItem
  [this props]
  {:ident         [:todo/id :todo/id]
   :query         [:todo/id :todo/title :todo/completed]}
  ...)

(defsc TodoSimpleDemo [this props]
  {:ident         (fn [] [::root "singleton"])
   :query         [{:all-todo-items (fp/get-query TodoItem)}]}
  ...)

Notice that on TodoItem we namespaced the keys. This is fine, as the integration code will strip these from the query. If TodoSimpleDemo were your root component, the query for it is already compatible with our defined API when using our GraphQL network:

(fulcro/new-fulcro-client
  :started-callback
  (fn [app]
    (df/load app :all-todo-items todo/TodoItem {:target [::root "singleton" :all-todo-items]}))

  :networking
  {:remote (pfn/graphql-network "https://api.graph.cool/simple/v1/cjjkw3slu0ui40186ml4jocgk")})

Mutations are similarly easy. The network component translates them as discussed earlier, so doing something like adding a new todo item likes like this:

(fm/defmutation create-todo-item [todo]
  (action [env] ...local optimistic stuff...)
  (remote [{:keys [ast]}]
    ;; Don't send the UI-specific params to the server...just the id and title
    (update ast :params select-keys [:todo/id :todo/title])))

The full source is shown below, but hopefully you can see how simple it is to get something going pretty quickly.

link:../src-docs/com/wsscode/pathom/book/graphql/fulcro_network/graphql_todo.cljs[role=include]

6.2. GraphQL and Connect

The more powerful way to use GraphQL from Pathom is to use it with Connect. This gives you the basic features you saw in the simple version, but also gives you a lot more power and extensibility.

The integration has a bit of boilerplate, but it’s all relatively simple. Please make sure you already understand Connect before reading this.

6.2.1. Keywords and GraphQL – Prefixes

In order to properly generate indexes Connect needs to know how you will prefix them for a given GraphQL endpoint. From there, the keyword also gives an indication of the "type" and attribute name.

Say we are interfacting with GitHub: we might choose the prefix github. Then our keywords would need to be things like :github.user/name. The name portion of the keyword can be hyphenated, and Pathom will turn that into a camel-case name, since that is the standard for GraphQL. Name munging is customizable via Pathom settings.

You will have to formally declare the prefix you’ve decided on in order to Connect to work.

6.2.2. GraphQL Entry Points and Connect Ident Maps

In GraphQL the schema designer indicates what entry points are possible. In GitHub’s public API you can, for example, access a User if you know their login. You can access a Repository if you know both the owner and the repository name.

You might wish to take a moment, log into GitHub, and play with these at https://developer.github.com/v4/explorer.

To look at a user, you need something like this:

query {
   user(login:"wilkerlucio") {
    createdAt
  }
}

To look at a repository, you need something like this:

query {
  repository(owner:"wilkerlucio" name:"pathom") {
    createdAt
  }
}

Our EDN queries use idents to stand for these kind of entry points. So, we’d like to be able to translate an EDN query like this:

[{[:github.user/login "wilkerlucio"] [:github.user/created-at]}]

into the GraphQL query above. This is the purpose of the "Ident Map". It is a map whose top-level keys are GraphQL entry point names, and whose value is a map of the attributes required at that entry point associated with EDN keywords:

{ENTRY-POINT-NAME {ATTR connect-keyword
                   ...}
 ...}

So, an ident map for the above two GraphQL entry points is:

{"user"       {"login" :github.user/login}
 "repository" {"owner" :github.user/login
               "name"  :github.repository/name}}

Installing such an ident map (covered shortly) will enable this feature.

If an entry point requires more than one input (as repository does), then there is no standard EDN ident that can directly use it. We’ll cover how to handle that in Multiple Input Entry Points

Interestingly, this feature of Pathom gives you an ability on GraphQL that GraphQL itself doesn’t have: the ability to nest an entry point anywhere in the query. GraphQL only understands entry points at the root of the query, but our EDN notation allows you to use an ident on a join at any level. Pathom Connect will correctly interpret such a join, process it against the GraphQL system, and properly nest the result.

6.2.3. Setting Up Connect with GraphQL

Now that you understand entry points we can explain the rest of the setup. A lot of it is just the standard Connect stuff, but of course there are additions for GraphQL.

First, you need to declare a place to store the indexes, a way to build your own resolvers, and (optionally) a way to inject your own custom mutations into the Connect processing system.

(defonce indexes (atom {::pc/idents #{::root}}))
(defmulti resolver-fn pc/resolver-dispatch)
(def defresolver (pc/resolver-factory resolver-fn indexes))
(defmulti mutation-fn pc/mutation-dispatch)
(def defmutation (pc/mutation-factory mutation-fn indexes))

Our base environment is very much like before:

(def base-env
  {::p/reader             [p/map-reader pc/all-async-readers]
   ::pc/resolver-dispatch resolver-fn
   ::pc/mutate-dispatch   mutation-fn
   ::p.http/driver        p.http.fetch/request-async})

but also need to create and configure a GraphQL Resolver:

(def github-gql
  {::pcg/resolver  `github-graphql
   ::pcg/url       (str "https://api.github.com/graphql?access_token=" (ls/get ::github-token))
   ::pcg/prefix    "github"
   ::pcg/ident-map {"user"       {"login" :github.user/login}
                    "repository" {"owner" :github.user/login
                                  "name"  :github.respository/name}}
   ::p.http/driver p.http.fetch/request-async})

(pcg/defgraphql-resolver base-env github-gql)
::pcg/resolver

A symbol. Required, but used primarily for debugging.

::pcg/url

The GraphQL API endpoint

::pcg/prefix

The prefix you’ll use in your EDN queries and mutations.

::pcg/ident-map

The definition of GraphQL entry points, as discussed previously.

::p.http/driver

A driver that can run HTTP requests. Used to issue requests (e.g. fetch schema).

The defgraphql-resolver basically creates multimethods for the mutation and query, to be used by the parser.

We’re using ls/get to pull our github access token from browser local storage so we don’t have to check it into code, and so anyone can use the example unedited. In Chrome, you can set this via the developer tools "Application" tab (once at the page for your app). Click on local storage, then add a key value pair. The key should be the keyword (typed out), and the value must be a QUOTED token (e.g. "987398ahbckjhbas"). The quotes are required!

Next, we need to create an async parser. This will essentially be basically this:

(defn create-parser []
  (p/async-parser {::p/env     base-env
                   ::p/mutate  pc/mutate-async
                   ::p/plugins [(p/env-wrap-plugin #(assoc % ::pc/indexes @indexes))
                                p/error-handler-plugin
                                p/request-cache-plugin
                                ;; optional: adds profile info at each step of the query. Requires you query for profile data.
                                pp/profile-plugin]}))

6.2.4. Loading the GraphQL Schema and Creating a Remote

The final setup step is to make sure that you load the GraphQL schema into the Connect indexes. If you’re using Fulcro it looks like this:

(new-fulcro-client
  :started-callback
  (fn [app]
    (go-catch
      (try
        (let [idx (<? (pcg/load-index github-gql))]
          (swap! indexes pc/merge-indexes idx))
        (catch :default e (js/console.error "Error making index" e)))))

  :networking
  {:remote (-> (create-parser)
               (pfn/pathom-remote)
               ;; OPTIONAL: Automatically adds profile queries to all outgoing queries, so you see profiling from the parser
               (pfn/profile-remote))}}

6.2.5. Adding Resolvers

Of course we’ve done all of this setup so we can make use of (and extend the capabilities of) some GraphQL API.

The normal stuff is trivial: Make EDN queries that ask for the proper attributes in the proper context.

In our example, we might want to list some information about some repositories. If you remember, repositories take two pieces of information, and idents can supply only one.

That’s ok, we can define a resolver for a root-level Connect property that can pre-establish some repositories into our context!

(defresolver `repositories
  {::pc/output [{:demo-repos [:github.user/login :github.repository/name]}]}
  (fn [_ _]
    {:demo-repos
     [{:github.user/login "wilkerlucio" :github.repository/name "pathom"}
      {:github.user/login "fulcrologic" :github.repository/name "fulcro"}
      {:github.user/login "fulcrologic" :github.repository/name "fulcro-inspect"}
      {:github.user/login "fulcrologic" :github.repository/name "fulcro-css"}
      {:github.user/login "fulcrologic" :github.repository/name "fulcro-spec"}
      {:github.user/login "thheller" :github.repository/name "shadow-cljs"}]}))

Remember, once Connect has enough info in a context, it can fill in the remaining details. Our Ident Map indicates that if we have "user login" and "repository name", then we can get a repository. Thus, a resolver that outputs values for the keywords associated with those requirements is sufficient!

Now we can run a query on :demo-repos like [{:demo-repos [:github.repository/created-at]}], and walk the graph from there to anywhere allowed!

6.2.6. Queries

The queries that are supported "out of the box" are those queries that follow the allowed shape of the documented GraphQL schema for your API. The EDN queries in Fulcro might look like this:

(fp/defsc Repository
  [this {:github.repository/keys [id name-with-owner viewer-has-starred]}]
  {:ident         [:github.repository/id :github.repository/id]
   :query         [:github.repository/id :github.repository/name-with-owner :github.repository/viewer-has-starred]}
  ...)

(fp/defsc GraphqlDemo
  [this {:keys [demo-repos]}]
  {:query         [{:demo-repos (fp/get-query Repository)}]}
  (dom/div
    (mapv repository demo-repos)))

All of Connect’s additinal features (placeholder nodes, augmenting the graph, reshaping) are now also easily accessible.

6.2.7. Fulcro Mutations and Remote

If you’re using Fulcro, then the normal method of defining mutations will work if you use the remote shown earlier. You simply prefix the mutation name with your GraphQL prefix and it’ll work:

(fm/defmutation github/add-star [_]
  (action [{:keys [state ref]}] ...)
  (remote [_] true))
This is not the defmutation we showed earlier in the setup. This is Fulcro’s defmutation.

You can, of course, modify the parameters, do mutation joins, etc.

6.2.8. Connect-Based Mutations (Advanced)

It is possible that you might want to define a mutation that is not on the GraphQL API, but which does some alternative remote operation. This is what the defmutation in our earlier setup was about (not to be confused with Fulcro’s defmutation macro).

The notation is the same as for resolvers:

(defmutation 'custom-mutation
  {::pc/params [:id {:boo [:y]}]                            ;; future autocomplete...noop now
   ::pc/output [:x]}                                        ;; future autocomplete...
  (fn [_ params]
    ;; can be async or sync.
    (async/go ...)))

Note: The params and output are currently meant as documentation. In an upcoming version they’ll also be leveraged for tool autocomplete.

The fn of the mutation can return a value (sync) or a channel (async). This means that the custom mutation could do something like hit an alternate REST API. This allows you to put in mutations that the async parser understands and allows to be integrated into a single expression (and API), even though they are not part of the GraphQL API you’re interacting with.

Of course, if you’re using Fulcro or Om Next, then you’ll also have to make sure they’re OK with the mutation symbolically (e.g. define a fm/defmutation as well).

6.2.9. Multiple Input Entry Points (Advanced)

Earlier we talked about how the Ident Map might specify GraphQL endpoints the required more than one parameter, and the fact that EDN idents only really have a spot for one bit of data beyond the keyword: [keyword value].

Sometimes we have cases like GitHub’s repository entry point where more than one parameter is required.

This can be gracefully handled with EDN query parameters if you modify how Connect processes the query.

Here are the steps:

  1. Create an ident reader that can put parameter data into the parsing context.

  2. Add the custom ident reader to the Connect readers.

  3. Pass the additional data via parameters (typically write a helper to make it pretty).

Custom Ident Reader
Since version 2.2.0-beta11 the pathon connect ident-reader got support for extra context using the parameter :pathom/context, so you don’t have to do any of the setup described here, but I think it still interesting to demonstrate how to write a custom ident reader, so this section will still live here.

An ident reader is just what it sounds like: it’s a reader that, when called on an ident, does some processing for it. The default ident reader in connect just detects the ident and does a join. It looks like this:

(defn ident-reader [env]
  (if-let [ent (indexed-ident env)]
    (p/join (atom ent) env)
    ::p/continue))

Now, remember that this query:

[{[:github.repository/name "n"] [...]}]

cannot work because there is only one of the required two bits of info (we also need owner).

What we’re going to do is allow parameters to make up the difference. If you unfamiliar with them, you just surround the element of the query in a list and add a map of params, like this:

'[{([:github.repository/name "n"] {:x v}) [...]}]

The problem, of course, is that this is really hard on the eyes. A bit too much nesting soup, and you need the quote ' in order to prevent an attempt to run a function! But this is what we need to allow us to add in more information. We can clean up the notation by defining a helper function:

(defn repository-ident
  "Returns a parameterized ident that can be used as a join key to directly query a repository."
  [owner name]
  (list [:github.repository/name name] {:entry-point-context {:github.user/login owner}}))

where we’ve chosen :entry-point-context to be the parameter name that will hold the extra stuff we want to push into the parsing context.

Now we can write a reasonable query that contains everything we need:

[{(repository-ident "joe" "boo") [:github.repository/created-at]}]

Now all that remains is to get Connect to understand it!

In our case we want the ident reader to look for the :entry-point-context parameter and plug it into the parsing context. That looks like this:

(defn parameterized-ident-reader [env]
  (if-let [context-entity (when-let [attr (p/ident-key env)]
                            {attr (p/ident-value env)})]
    (let [extra-args (get-in env [:ast :params :entry-point-context])
          ent        (cond-> context-entity
                       extra-args (merge extra-args))]
      (p/join ent env))
    ::p/continue))

Of course we need to install it as well. In our original setup we had:

(def base-env
  {::p/reader             [p/map-reader pc/all-async-readers]
   ::pc/resolver-dispatch resolver-fn
   ::pc/mutate-dispatch   mutation-fn
   ::p.http/driver        p.http.fetch/request-async})

If you look at pc-all-async-readers, you’ll see that it is just:

(def all-async-readers [async-reader ident-reader index-reader])

so we just change our base environment:

(def base-env
  {::p/reader             [p/map-reader pc/async-reader parameterized-ident-reader pc/index-reader]
   ::pc/resolver-dispatch resolver-fn
   ::pc/mutate-dispatch   mutation-fn
   ::p.http/driver        p.http.fetch/request-async})

and we’re good to go!

6.3. Complete GraphQL Connect Example

A complete working example (for workspaces) is shown below:

link:../workspaces/src/com/wsscode/pathom/workspaces/graphql/github_demo.cljs[role=include]

6.4. EDN→GraphQL

Here you can try an interactive convertor. Type your EDN graph query on the left side and see the GraphQL equivalent been generated on the right.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close