Sprinkling in some server-side
Well sort of
If you squint really hard
Two weeks ago we were poking around Fulcro with the frontend, now let's check it out on the "backend", with our star performer, Pathom.
You've probably noticed that I've written backend in air-quotes, well I thought about it a lot, well ok a little bit and decided that I wanted to stick to introducing as few things as possible, so in that vein, we're going to use a pretty nifty feature that pathom has, you can run it on the client 😁...
Now, this has some cool implications, you can use pathom to manage talking to all kinds of other external api's for you via rest interfaces or graph api's or err SOAP (here be dragons), but we're not going to do any of that =)...
We're just going to pretend that our pathom parser can see some db and talk to it...
Let's get started
Here's an updated github repo for you to follow along with the code =)...
We're working with some new concept, so ultimately we should create a new namespace
.
That btw tends to be a rule I like to follow to keep my programs easy to think about:
new concept = new namespace
Let's be really unimaginative and call it pathom.cljs
:
(ns fulcro-starter.pathom
(:require [com.wsscode.pathom.core :as p]
[com.wsscode.pathom.connect :as pc]))
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [:a-list-of-things]}
{:a-list-of-things [{:a-thing "1 stuff" :fav-colour "red"}
{:a-thing "two stuff" :fav-colour "blue"}
{:a-thing "\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" :fav-colour "green"}]})
(def all-resolvers
[a-list-of-things-resolver])
(def parser
(p/parser
{::p/env {::p/reader [p/map-reader
pc/reader2
pc/open-ident-reader
p/env-placeholder-reader]
::p/placeholder-prefixes #{">"}}
::p/plugins [(pc/connect-plugin {::pc/register all-resolvers})
p/error-handler-plugin
p/trace-plugin]}))
We've gone back to our not so excellent example of a-list-of-things
comprised of a-thing
's.
Pulling data out
Now how do we get something out of this?
(parser {} [:a-list-of-things])
#_#_=>
{:a-list-of-things [{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}
{:a-thing "💥💥💥 stuff", :fav-colour "green"}]}
Great! So know how to get out our a-list-of-things
.
So now, what's going on here?
Well, what we have is a parser
which pathom
gives us that allows us to request data of a specific shape, and get back output of whatever data we can access in that shape.
In many ways, pathom fulfils a role similar to what graphql gives us if you're familiar with that, but with some differences, one of which we'll cover in a little bit.
Pathom does this by allowing you to define a resolvers
using defresolver
, any defined resolvers
you give to pathom's register
during its parser
definition can be used by pathom
to answer your query
.
What's a query
?
Well, the last query
we used was:
[:a-list-of-things]
In this query we asked pathom to give us something for :a-list-of-things
, pathom replied with:
{:a-list-of-things [{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}
{:a-thing "💥💥💥 stuff", :fav-colour "green"}]}
Great!
But that's too much I'm getting back! I hear you say, I just wanted :fav-colour
!
Well we can ask for that specifically:
(parser {} [{:a-list-of-things [:fav-colour]}])
#_#_=> {:a-list-of-things [{:fav-colour "red"} {:fav-colour "blue"} {:fav-colour "green"}]}
Now how does this all work?
Well, we've introduced a level of nesting because we wanted to provide some more detail, we've gone from this:
[:a-list-of-things]
to this:
[{:a-list-of-things [:fav-colour]}]
Pathom interprets a map that's provided in a query as a join and will try and use a resolver to satisfy whatever is queried within the context of that join.
Ok, well what happens if we ask for some information pathom doesn't know about?
(parser {} [{:a-list-of-things [:fav-colour :fav-shape]}])
#_#_=>
{:a-list-of-things [{:fav-colour "red", :fav-shape :com.wsscode.pathom.core/not-found}
{:fav-colour "blue", :fav-shape :com.wsscode.pathom.core/not-found}
{:fav-colour "green", :fav-shape :com.wsscode.pathom.core/not-found}]}
As you can see pathom just tells us that it doesn't have that information using :com.wsscode.pathom.core/not-found
.
Now let's examine that resolver we started with in more detail:
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [:a-list-of-things]}
{:a-list-of-things [{:a-thing "1 stuff" :fav-colour "red"}
{:a-thing "two stuff" :fav-colour "blue"}
{:a-thing "\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" :fav-colour "green"}]})
So we have a ::pc/output [:a-list-of-things]
, what's that all about?
Well let's play around with this resolver by altering slightly:
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [:a-list-of-things]}
{:a-list-of-things [{:a-thing "1 stuff" :fav-colour "red"}
{:a-thing "two stuff" :fav-colour "blue"}
{:a-thing "\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" :fav-colour "green"}]
:some-other-things [{:a-thing "other stuff" :fav-shape "square"}]})
Now before we go any further we should mention something, pathom caches whatever resolver definitions you've given it, so if you alter a definition like we're doing here, you must reload the namespace that contains the resolver, as well as the one that contains the parser! This is important, or you'll have old resolver definitions in your parser and won't be able to see any changes you've made to the resolver definitions.
This is less of a concern for us now as we're just working entirely in the fulcro-starter.pathom
namespace, but if you start building a bigger application this sort of thing can trip you up! I know it's tripped me up a few times and I'm still trying to work out how to make mount
et al work nicely with this.
So to continue from that little digression, let's reload the namespace fulcro-starter.pathom
.
Now let's try our original query to check everything's working fine:
(parser {} [:a-list-of-things])
#_#_=>
{:a-list-of-things [{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}
{:a-thing "💥💥💥 stuff", :fav-colour "green"}]}
Looks like nothing's changed on that front.
So let's try getting :some-other-things
!
(parser {} [:some-other-things])
#_#_=> {:some-other-things :com.wsscode.pathom.core/not-found}
Huh, that's interesting. So pathom can't find it.
Let's adjust our ::pc/output
in our resolver.
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [:a-list-of-things :some-other-things]}
{:a-list-of-things [{:a-thing "1 stuff" :fav-colour "red"}
{:a-thing "two stuff" :fav-colour "blue"}
{:a-thing "\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" :fav-colour "green"}]
:some-other-things [{:a-thing "other stuff" :fav-shape "square"}]})
Now let's reload our namespace again and call our query.
(parser {} [:some-other-things])
#_#_=> {:some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}
Great! So we've learned something, pathom uses the ::pc/output
to work out what resolvers can resolve. So not having a key listed in our ::pc/output
can mean that even though we might be able to resolve it, pathom doesn't know we can.
The trace
Now's probably a good time to look at what the p/trace-plugin
does:
So what if we didn't know what either of those queries were doing? That's what tracing is for!
Let's pass in :com.wsscode.pathom/trace
to our query:
(parser {} [:a-list-of-things :com.wsscode.pathom/trace])
#_#_=>
{:a-list-of-things [{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}
{:a-thing "💥💥💥 stuff", :fav-colour "green"}],
:com.wsscode.pathom/trace {:start 0,
:path [],
:duration 4,
:details [{:event "trace-done", :duration 0, :start 4}],
:children [{:start 0,
:path [:a-list-of-things],
:duration 2,
:details [{:event "compute-plan",
:duration 1,
:start 1,
:plan (([:a-list-of-things
fulcro-starter.pathom/a-list-of-things-resolver])),
:provides #{:a-list-of-things}}
{:event "call-resolver",
:duration 0,
:start 2,
:key :a-list-of-things,
:label "fulcro-starter.pathom/a-list-of-things-resolver",
:input-data {},
:sym fulcro-starter.pathom/a-list-of-things-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 2,
:key :a-list-of-things,
:sym fulcro-starter.pathom/a-list-of-things-resolver}],
:name ":a-list-of-things"}
{:start 2,
:path [:com.wsscode.pathom/trace],
:duration 1,
:details [{:event "compute-plan",
:duration 0,
:start 3,
:plan (([:com.wsscode.pathom/trace com.wsscode.pathom.trace/trace])),
:provides #{:com.wsscode.pathom/trace}}
{:event "call-resolver",
:duration 0,
:start 3,
:key :com.wsscode.pathom/trace,
:label "com.wsscode.pathom.trace/trace",
:input-data {},
:sym com.wsscode.pathom.trace/trace}
{:event "merge-resolver-response",
:duration 0,
:start 3,
:key :com.wsscode.pathom/trace,
:sym com.wsscode.pathom.trace/trace}],
:name ":com.wsscode.pathom/trace"}],
:hint "Query"}}
Now, this may take you a little while to get used to reading and pathom does also offer pathom-viz which will provide a timeline visualisation as well.
So we can see that the :duration
key shows the time taken in ms for that operation.
Let's take a look at the plan breakdown in more detail:
[{:event "compute-plan",
:duration 1,
:start 1,
:plan (([:a-list-of-things
fulcro-starter.pathom/a-list-of-things-resolver])),
:provides #{:a-list-of-things}}
{:event "call-resolver",
:duration 0,
:start 2,
:key :a-list-of-things,
:label "fulcro-starter.pathom/a-list-of-things-resolver",
:input-data {},
:sym fulcro-starter.pathom/a-list-of-things-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 2,
:key :a-list-of-things,
:sym fulcro-starter.pathom/a-list-of-things-resolver}]
Pathom computes a plan, calls the resolver, gets a response and merges it back into the result.
Let's contrast this to a trace I took earlier when :some-other-things
was not defined:
(parser {} [:some-other-things :com.wsscode.pathom/trace])
#_#_=>
{:some-other-things :com.wsscode.pathom.core/not-found,
:com.wsscode.pathom/trace {:start 0,
:path [],
:duration 2,
:details [{:event "trace-done", :duration 0, :start 2}],
:children [{:start 0,
:path [:some-other-things],
:duration 1,
:details [{:event "compute-plan", :duration 1, :start 0}],
:name ":some-other-things"}
{:start 1,
:path [:com.wsscode.pathom/trace],
:duration 1,
:details [{:event "compute-plan",
:duration 0,
:start 1,
:plan (([:com.wsscode.pathom/trace com.wsscode.pathom.trace/trace])),
:provides #{:com.wsscode.pathom/trace}}
{:event "call-resolver",
:duration 1,
:start 1,
:key :com.wsscode.pathom/trace,
:label "com.wsscode.pathom.trace/trace",
:input-data {},
:sym com.wsscode.pathom.trace/trace}
{:event "merge-resolver-response",
:duration 0,
:start 2,
:key :com.wsscode.pathom/trace,
:sym com.wsscode.pathom.trace/trace}],
:name ":com.wsscode.pathom/trace"}],
:hint "Query"}}
Taking the relevant segment out:
{:start 0,
:path [:some-other-things],
:duration 1,
:details [{:event "compute-plan", :duration 1, :start 0}],
:name ":some-other-things"}
Pathom here just computes a plan, and because the :plan
key is absent that plan is empty. So it's got nowhere to go on that step and just is going to say :com.wsscode.pathom.core/not-found
.
The trace isn't very deep however, so let's refactor our resolvers a little:
(ns fulcro-starter.pathom
...)
(def a-things-data
{"1 stuff" {:a-thing "1 stuff" :fav-colour "red"}
"two stuff" {:a-thing "two stuff" :fav-colour "blue"}
"\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" {:a-thing "\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" :fav-colour "green"}
"other stuff" {:a-thing "other stuff" :fav-shape "square"}})
(def a-list-of-things-data
#{"1 stuff"
"two stuff"
"\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff"})
(def some-other-things-data
#{"other stuff"})
(pc/defresolver a-thing-resolver [_ params]
{::pc/input #{:a-thing}
::pc/output [:a-thing :fav-colour :fav-shape]}
(get a-things-data (:a-thing params)))
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [{:a-list-of-things [:a-thing]} {:some-other-things [:a-thing]}]}
{:a-list-of-things (mapv (fn [id] {:a-thing id}) a-list-of-things-data)})
(pc/defresolver some-other-things-resolver [_ _]
{::pc/output [{:a-list-of-things [:a-thing]} {:some-other-things [:a-thing]}]}
{:some-other-things (mapv (fn [id] {:a-thing id}) some-other-things-data)})
(def all-resolvers
[a-thing-resolver a-list-of-things-resolver some-other-things-resolver])
...
There, now it looks like these resolvers could be calling some db etc to get the data they're requesting. They're not of course, but they could be =)...
Make special note that a-list-of-things-resolver
and some-other-things-resolver
return something of the form {:a-thing id}
.
Try and do that as a rule, have your resolvers doing the least work necessary to get the data they need.
That means pathom can intelligently use it's planner to call only for the information that it needs.
Now let's check that everything's working fine:
(parser {} [:a-list-of-things])
#_#_=> {:a-list-of-things [{:a-thing "💥💥💥 stuff"} {:a-thing "1 stuff"} {:a-thing "two stuff"} {:a-thing "four stuff"}]}
Huh, that's odd, what happened to :fav-colour
?
Let's do a trace!
(parser {} [:a-list-of-things :com.wsscode.pathom/trace])
#_#_=>
{:a-list-of-things [{:a-thing "💥💥💥 stuff"} {:a-thing "1 stuff"} {:a-thing "two stuff"} {:a-thing "four stuff"}],
:com.wsscode.pathom/trace {:start 39,
:path [],
:duration 11,
:details [{:event "trace-done", :duration 0, :start 50}],
:children [{:start 46,
:path [:a-list-of-things],
:duration 3,
:details [{:event "compute-plan",
:duration 1,
:start 48,
:plan (([:a-list-of-things
fulcro-starter.pathom/a-list-of-things-resolver])),
:provides #{:a-list-of-things}}
{:event "call-resolver",
:duration 0,
:start 49,
:key :a-list-of-things,
:label "fulcro-starter.pathom/a-list-of-things-resolver",
:input-data {},
:sym fulcro-starter.pathom/a-list-of-things-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 49,
:key :a-list-of-things,
:sym fulcro-starter.pathom/a-list-of-things-resolver}],
:name ":a-list-of-things"}
{:start 49,
:path [:com.wsscode.pathom/trace],
:duration 1,
:details [{:event "compute-plan",
:duration 0,
:start 49,
:plan (([:com.wsscode.pathom/trace com.wsscode.pathom.trace/trace])),
:provides #{:com.wsscode.pathom/trace}}
{:event "call-resolver",
:duration 0,
:start 50,
:key :com.wsscode.pathom/trace,
:label "com.wsscode.pathom.trace/trace",
:input-data {},
:sym com.wsscode.pathom.trace/trace}
{:event "merge-resolver-response",
:duration 0,
:start 50,
:key :com.wsscode.pathom/trace,
:sym com.wsscode.pathom.trace/trace}],
:name ":com.wsscode.pathom/trace"}],
:hint "Query"}}
As we look at our plan we see that a-thing-resolver
isn't being called.
Thinking about it, that makes sense!
We've not told pathom anything other than just give us back whatever's at :a-list-of-things
.
Let's be a bit more specific:
(parser {} [{:a-list-of-things [:a-thing :fav-colour]}])
#_#_=>
{:a-list-of-things [{:a-thing "💥💥💥 stuff", :fav-colour "green"}
{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}]}
That's better =)...
And now :some-other-things
in a similar way:
(parser {} [{:some-other-things [:a-thing :fav-shape]}])
=> {:some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}
Looks good =)...
Now let's check out that trace:
(parser {} [{:a-list-of-things [:a-thing :fav-colour]} :com.wsscode.pathom/trace])
#_#_=>
{:a-list-of-things [{:a-thing "💥💥💥 stuff", :fav-colour "green"}
{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}],
:com.wsscode.pathom/trace {:start 0,
:path [],
:duration 12,
:details [{:event "trace-done", :duration 0, :start 12}],
:children [{:start 0,
:path [:a-list-of-things],
:duration 2,
:details [{:event "compute-plan",
:duration 1,
:start 0,
:plan (([:a-list-of-things
fulcro-starter.pathom/a-list-of-things-resolver])
([:a-list-of-things
fulcro-starter.pathom/some-other-things-resolver])),
:provides #{:a-list-of-things :some-other-things}}
{:event "call-resolver",
:duration 0,
:start 1,
:key :a-list-of-things,
:label "fulcro-starter.pathom/a-list-of-things-resolver",
:input-data {},
:sym fulcro-starter.pathom/a-list-of-things-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 2,
:key :a-list-of-things,
:sym fulcro-starter.pathom/a-list-of-things-resolver}],
:name ":a-list-of-things",
:children [{:start 2,
:path [:a-list-of-things 0],
:duration 3,
:details [],
:name "0",
:children [{:start 4,
:path [:a-list-of-things 0 :a-thing],
:duration 0,
:details [],
:name ":a-thing"}
{:start 4,
:path [:a-list-of-things 0 :fav-colour],
:duration 3,
:details [{:event "compute-plan",
:duration 1,
:start 4,
:plan (([:fav-colour
fulcro-starter.pathom/a-thing-resolver])),
:provides #{:fav-colour :fav-shape}}
{:event "call-resolver",
:duration 1,
:start 6,
:key :fav-colour,
:label "fulcro-starter.pathom/a-thing-resolver",
:input-data {:a-thing "💥💥💥 stuff"},
:sym fulcro-starter.pathom/a-thing-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 7,
:key :fav-colour,
:sym fulcro-starter.pathom/a-thing-resolver}],
:name ":fav-colour"}]}
{:start 2,
:path [:a-list-of-things 1],
:duration 2,
:details [],
:name "1",
:children [{:start 7,
:path [:a-list-of-things 1 :a-thing],
:duration 0,
:details [],
:name ":a-thing"}
{:start 8,
:path [:a-list-of-things 1 :fav-colour],
:duration 1,
:details [{:event "compute-plan",
:duration 0,
:start 8,
:plan (([:fav-colour
fulcro-starter.pathom/a-thing-resolver])),
:provides #{:fav-colour :fav-shape}}
{:event "call-resolver",
:duration 0,
:start 8,
:key :fav-colour,
:label "fulcro-starter.pathom/a-thing-resolver",
:input-data {:a-thing "1 stuff"},
:sym fulcro-starter.pathom/a-thing-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 9,
:key :fav-colour,
:sym fulcro-starter.pathom/a-thing-resolver}],
:name ":fav-colour"}]}
{:start 2,
:path [:a-list-of-things 2],
:duration 2,
:details [],
:name "2",
:children [{:start 9,
:path [:a-list-of-things 2 :a-thing],
:duration 0,
:details [],
:name ":a-thing"}
{:start 9,
:path [:a-list-of-things 2 :fav-colour],
:duration 1,
:details [{:event "compute-plan",
:duration 0,
:start 10,
:plan (([:fav-colour
fulcro-starter.pathom/a-thing-resolver])),
:provides #{:fav-colour :fav-shape}}
{:event "call-resolver",
:duration 0,
:start 10,
:key :fav-colour,
:label "fulcro-starter.pathom/a-thing-resolver",
:input-data {:a-thing "two stuff"},
:sym fulcro-starter.pathom/a-thing-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 10,
:key :fav-colour,
:sym fulcro-starter.pathom/a-thing-resolver}],
:name ":fav-colour"}]}]}
{:start 11,
:path [:com.wsscode.pathom/trace],
:duration 1,
:details [{:event "compute-plan",
:duration 1,
:start 11,
:plan (([:com.wsscode.pathom/trace com.wsscode.pathom.trace/trace])),
:provides #{:com.wsscode.pathom/trace}}
{:event "call-resolver",
:duration 0,
:start 12,
:key :com.wsscode.pathom/trace,
:label "com.wsscode.pathom.trace/trace",
:input-data {},
:sym com.wsscode.pathom.trace/trace}
{:event "merge-resolver-response",
:duration 0,
:start 12,
:key :com.wsscode.pathom/trace,
:sym com.wsscode.pathom.trace/trace}],
:name ":com.wsscode.pathom/trace"}],
:hint "Query"}}
Now we can see that when it computes a plan, that plan is much more involved.
Oh! What's going on here? Pathom is telling is that to find :a-list-of-things
, it needs to get a-list-of-things-resolver
and some-other-things-resolver
. What's happened?
Let's look at our resolver definitions:
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [{:a-list-of-things [:a-thing]} {:some-other-things [:a-thing]}]}
{:a-list-of-things (mapv (fn [id] {:a-thing id}) a-list-of-things-data)})
(pc/defresolver some-other-things-resolver [_ _]
{::pc/output [{:a-list-of-things [:a-thing]} {:some-other-things [:a-thing]}]}
{:some-other-things (mapv (fn [id] {:a-thing id}) some-other-things-data)})
Well whoops! 😳
Looks like we've made a mistake, our ::pc/output
for both the resolvers is wrong! They're declaring that they can provide more information that they can.
Let's fix that:
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [{:a-list-of-things [:a-thing]}]}
{:a-list-of-things (mapv (fn [id] {:a-thing id}) a-list-of-things-data)})
(pc/defresolver some-other-things-resolver [_ _]
{::pc/output [{:some-other-things [:a-thing]}]}
{:some-other-things (mapv (fn [id] {:a-thing id}) some-other-things-data)})
Ok, now let's reload our namespace and trace again!
(parser {} [{:a-list-of-things [:a-thing :fav-colour]} :com.wsscode.pathom/trace])
#_#_=>
{:a-list-of-things [{:a-thing "💥💥💥 stuff", :fav-colour "green"}
{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}],
:com.wsscode.pathom/trace {:start 1,
:path [],
:duration 14,
:details [{:event "trace-done", :duration 0, :start 15}],
:children [{:start 1,
:path [:a-list-of-things],
:duration 1,
:details [{:event "compute-plan",
:duration 1,
:start 1,
:plan (([:a-list-of-things
fulcro-starter.pathom/a-list-of-things-resolver])),
:provides #{:a-list-of-things}}
{:event "call-resolver",
:duration 0,
:start 2,
:key :a-list-of-things,
:label "fulcro-starter.pathom/a-list-of-things-resolver",
:input-data {},
:sym fulcro-starter.pathom/a-list-of-things-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 2,
:key :a-list-of-things,
:sym fulcro-starter.pathom/a-list-of-things-resolver}],
:name ":a-list-of-things",
:children [{:start 3,
:path [:a-list-of-things 0],
:duration 4,
:details [],
:name "0",
:children [{:start 5,
:path [:a-list-of-things 0 :a-thing],
:duration 0,
:details [],
:name ":a-thing"}
{:start 5,
:path [:a-list-of-things 0 :fav-colour],
:duration 4,
:details [{:event "compute-plan",
:duration 1,
:start 5,
:plan (([:fav-colour
fulcro-starter.pathom/a-thing-resolver])),
:provides #{:fav-colour :fav-shape}}
{:event "call-resolver",
:duration 0,
:start 8,
:key :fav-colour,
:label "fulcro-starter.pathom/a-thing-resolver",
:input-data {:a-thing "💥💥💥 stuff"},
:sym fulcro-starter.pathom/a-thing-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 9,
:key :fav-colour,
:sym fulcro-starter.pathom/a-thing-resolver}],
:name ":fav-colour"}]}
{:start 3,
:path [:a-list-of-things 1],
:duration 2,
:details [],
:name "1",
:children [{:start 9,
:path [:a-list-of-things 1 :a-thing],
:duration 0,
:details [],
:name ":a-thing"}
{:start 9,
:path [:a-list-of-things 1 :fav-colour],
:duration 2,
:details [{:event "compute-plan",
:duration 0,
:start 10,
:plan (([:fav-colour
fulcro-starter.pathom/a-thing-resolver])),
:provides #{:fav-colour :fav-shape}}
{:event "call-resolver",
:duration 1,
:start 10,
:key :fav-colour,
:label "fulcro-starter.pathom/a-thing-resolver",
:input-data {:a-thing "1 stuff"},
:sym fulcro-starter.pathom/a-thing-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 11,
:key :fav-colour,
:sym fulcro-starter.pathom/a-thing-resolver}],
:name ":fav-colour"}]}
{:start 3,
:path [:a-list-of-things 2],
:duration 2,
:details [],
:name "2",
:children [{:start 11,
:path [:a-list-of-things 2 :a-thing],
:duration 0,
:details [],
:name ":a-thing"}
{:start 11,
:path [:a-list-of-things 2 :fav-colour],
:duration 2,
:details [{:event "compute-plan",
:duration 0,
:start 12,
:plan (([:fav-colour
fulcro-starter.pathom/a-thing-resolver])),
:provides #{:fav-colour :fav-shape}}
{:event "call-resolver",
:duration 0,
:start 13,
:key :fav-colour,
:label "fulcro-starter.pathom/a-thing-resolver",
:input-data {:a-thing "two stuff"},
:sym fulcro-starter.pathom/a-thing-resolver}
{:event "merge-resolver-response",
:duration 0,
:start 13,
:key :fav-colour,
:sym fulcro-starter.pathom/a-thing-resolver}],
:name ":fav-colour"}]}]}
{:start 13,
:path [:com.wsscode.pathom/trace],
:duration 2,
:details [{:event "compute-plan",
:duration 1,
:start 13,
:plan (([:com.wsscode.pathom/trace com.wsscode.pathom.trace/trace])),
:provides #{:com.wsscode.pathom/trace}}
{:event "call-resolver",
:duration 0,
:start 14,
:key :com.wsscode.pathom/trace,
:label "com.wsscode.pathom.trace/trace",
:input-data {},
:sym com.wsscode.pathom.trace/trace}
{:event "merge-resolver-response",
:duration 0,
:start 15,
:key :com.wsscode.pathom/trace,
:sym com.wsscode.pathom.trace/trace}],
:name ":com.wsscode.pathom/trace"}],
:hint "Query"}}
Now looking at the plan computed we can see that only a-list-of-things-resolver
is called.
Let's unpack a little what's going on here.
The trace above can broadly be broken down into these steps:
- what does
:a-list-of-things
want? - ok, it needs three items, what are they?
- so for the first item it needs
:a-thing
and:fav-colour
, we've got:a-thing
, how do we find out:fav-colour
? - then do 3. for the second and third item.
While it goes it merges in the data that it collects. Try and scan the plan above and see if you can spot where it's doing all the steps I've mentioned.
Ok, I think we've covered traces in a reasonable amount of detail, don't hesitate to reach for them if you find yourself wanting to understand what pathom's doing!
What about getting something?
All this time we've discussed getting a predefined thing, but what happens when we know what we want?
Well we need to join against a specified attribute like so:
(parser {} [{[:a-thing "💥💥💥 stuff"] [:a-thing :fav-colour]}])
#_#_=> {[:a-thing "💥💥💥 stuff"] {:a-thing "💥💥💥 stuff", :fav-colour "green"}}
That was pretty straightforward huh? =)...
We don't have a root
Let's discuss one difference between graphql and pathom.
pathom doesn't have a notion of a root. That's because pathom views the data it accesses as a graph, so you can do things like this:
(parser {} [{:a-list-of-things [:some-other-things]}])
#_#_=>
{:a-list-of-things [{:some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}
{:some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}
{:some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}]}
This must look really weird right? I mean what's going on?
Well perhaps this might shed some light on the matter:
(parser {} [{:a-list-of-things [:a-thing :some-other-things]}])
#_#_=>
{:a-list-of-things [{:a-thing "💥💥💥 stuff", :some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}
{:a-thing "1 stuff", :some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}
{:a-thing "two stuff", :some-other-things [{:a-thing "other stuff", :fav-shape "square"}]}]}
That's right, we're just telling pathom to find us :a-list-of-things
and for every entry also get us :some-other-things
.
So how does this work?
Well if you look at a-thing-resolver
vs a-list-of-things-resolver
and some-other-things-resolver
, you'll see a clear difference, spotted it?
a-thing-resolver
defines a ::pc/input
while the other two do not.
That's a big difference, that means a-list-of-things-resolver
and some-other-things-resolver
can be called anywhere within your query and they'll return their results.
Also, note that ::pc/input
defines a set! This is important!
Now, why is this useful? Sometimes you might want to be able to add some specific structure deep inside your query, the canonical example is to create a timestamp-resolver
:
(pc/defresolver timestamp-resolver [_ _]
{::pc/output [:timestamp]}
{:timestamp (.now js/Date)})
Just add the above resolver to all-resolvers
reload the namespace and now if you needed to you can get the timestamp within a query, you can!
(parser {} [{:a-list-of-things [:a-thing {:some-other-things [:fav-shape :timestamp]}]}])
#_#_=>
{:a-list-of-things [{:a-thing "💥💥💥 stuff", :some-other-things [{:fav-shape "square", :timestamp 1590525422451}]}
{:a-thing "1 stuff", :some-other-things [{:fav-shape "square", :timestamp 1590525422451}]}
{:a-thing "two stuff", :some-other-things [{:fav-shape "square", :timestamp 1590525422451}]}]}
The Placeholder prefix
Now what's this part all about?
::p/placeholder-prefixes #{">"}
Well that's a nifty feature of pathom =)...
Let's say for some reason we decided that we wanted to have our keys prefixed by :something
.
Maybe our client needed it in that structure, or you want to pull data from multiple api's and need to artificially add one level of nesting.
Well let's try it:
(parser {} [{:something [:a-list-of-things]}])
#_#_=> {:something :com.wsscode.pathom.core/not-found}
Uh oh, pathom doesn't know what to do with that.
Enter >
the placeholder prefix.
By using it as a namespace pathom knows that the key you're providing is just there to artifically add a level of nesting:
(parser {} [{:>/something [:a-list-of-things]}])
#_#_=> {:>/something {:a-list-of-things [{:a-thing "💥💥💥 stuff"} {:a-thing "1 stuff"} {:a-thing "two stuff"}]}}
One particularly nice use for it that I've found is that it allows you to create two views at the same level:
(parser {} [{:>/list [:a-list-of-things]}
{:>/some [:some-other-things]}])
#_#_=>
{:>/list {:a-list-of-things [{:a-thing "💥💥💥 stuff"} {:a-thing "1 stuff"} {:a-thing "two stuff"}]},
:>/some {:some-other-things [{:a-thing "other stuff"}]}}
Mutations
We've not discussed mutations at all, so let's go over that =)...
Let's update our code to allow us to call mutations:
(ns fulcro-starter.pathom
(:require [com.wsscode.pathom.core :as p]
[com.wsscode.pathom.connect :as pc]))
...
(pc/defresolver timestamp-resolver [_ _]
{::pc/output [:timestamp]}
{:timestamp (.now js/Date)})
(pc/defmutation add-thing [_ {:keys [a-thing] :as params}]
{::pc/params [:a-thing]}
(println :a-thing a-thing)
{:a-thing a-thing})
(def all-resolvers
[a-thing-resolver a-list-of-things-resolver some-other-things-resolver timestamp-resolver add-thing])
(def parser
(p/parser
{...
::p/mutate pc/mutate
...))
So our mutation to start with doesn't mutate anything, but let's build it up =)...
So we reload our namespace and call:
(parser {} [`(add-thing {:a-thing "four stuff"})])
:a-thing four stuff
#_#_=> {fulcro-starter.pathom/add-thing {:a-thing "four stuff"}}
Ok, so that's interesting, pathom returns a symbol of the mutation name as the key and then returns our mutation result, we'll cover why it does that in a little bit, but it's nice to note.
Now, since we're running a mutation we need to mutate something, so let's change some of our definitions:
(def a-things-data
(atom {"1 stuff" {:a-thing "1 stuff" :fav-colour "red"}
"two stuff" {:a-thing "two stuff" :fav-colour "blue"}
"\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" {:a-thing "\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff" :fav-colour "green"}
"other stuff" {:a-thing "other stuff" :fav-shape "square"}}))
(def a-list-of-things-data
(atom #{"1 stuff"
"two stuff"
"\uD83D\uDCA5\uD83D\uDCA5\uD83D\uDCA5 stuff"}))
(def some-other-things-data
(atom #{"other stuff"}))
(pc/defresolver a-thing-resolver [_ params]
{::pc/input #{:a-thing}
::pc/output [:a-thing :fav-colour :fav-shape]}
(get @a-things-data (:a-thing params)))
(pc/defresolver a-list-of-things-resolver [_ _]
{::pc/output [:a-list-of-things]}
{:a-list-of-things (mapv (fn [id] {:a-thing id}) @a-list-of-things-data)})
(pc/defresolver some-other-things-resolver [_ _]
{::pc/output [:some-other-things]}
{:some-other-things (mapv (fn [id] {:a-thing id}) @some-other-things-data)})
Now we can swap!
these definitions!
So let's modify our mutation to take advantage of this:
(pc/defmutation add-thing [_ {:keys [a-thing] :as params}]
{::pc/params [:a-thing]}
(println :a-thing a-thing)
(swap! a-things-data assoc a-thing params)
(swap! a-list-of-things-data conj a-thing)
{:a-thing a-thing})
Now let's reload our namespace ;)...
(parser {} [`(add-thing {:a-thing "four stuff"})])
:a-thing four stuff
#_#_=> {fulcro-starter.pathom/add-thing {:a-thing "four stuff"}}
Looks good, did it change?
(parser {} [:a-list-of-things])
#_#_=> {:a-list-of-things [{:a-thing "💥💥💥 stuff"} {:a-thing "1 stuff"} {:a-thing "two stuff"} {:a-thing "four stuff"}]}
Whoop! 🎉
Now, why was I so interested earlier in what pathom returned from the mutation?
Well because you can do this:
(parser {} [{`(add-thing {:a-thing "four stuff"})
[{:a-list-of-things [:a-thing :fav-colour]}]}])
:a-thing four stuff
=>
{fulcro-starter.pathom/add-thing {:a-list-of-things [{:a-thing "💥💥💥 stuff", :fav-colour "green"}
{:a-thing "1 stuff", :fav-colour "red"}
{:a-thing "two stuff", :fav-colour "blue"}
{:a-thing "four stuff",
:fav-colour :com.wsscode.pathom.core/not-found}]}}
So you if your mutation gives back a valid id-like thing you can chain your mutation into a query!
I think I've given you a reasonable overview of what the more server-side part of fulcro is like, but the best part is pathom's a library! You don't have to use it with fulcro =)... Just add it to a project when you want a way of querying complex data via a graphql-like api.