Resolvers are the primary building blocks in Pathom. In Pathom resolvers are used to express a relationship between attributes.
On this page, you will learn how to create resolvers and use them. After that you will learn about the built-in resolvers that come with Pathom.
What is a resolver in Pathom?
The Pathom engine gathers its resolution features from the relationship between attributes. Resolvers are the piece that expresses those relationships.
To put in more concrete terms, imagine that you have some user id, and you want to know
the user's birth year. In Pathom, we express this relationship using the attributes
:acme.user/birth-year for example.
Now we need to implement how to fetch the name from the id, this can be a lookup in a database, or a map, or any other way, doesn't matter. What matters is that you express this connection and provide code that makes it happen.
Here I'll illustrate this using a map lookup:
In Pathom, resolvers represent edges in a graph. As illustrated in the following figure:
I made the names short on the diagrams for space, but keep your domain qualifiers long.
This example was a simplification; most systems will store the full birth date instead, and then extract the year from this date.
We can simulate such case using the following setup:
Here is a diagram from the previous setup:
Given that image, let's consider that we know about some user id and want to know this user birthday, we can reach that using the path highlighted in the next diagram:
Here I'll show you one way of doing a request like this, using the Pathom EQL interface:
So resolvers establish edges in the Pathom graph, allowing Pathom to traverse from some source data to some target data. In the rest of this documentation, you will learn how to define resolvers.
defresolver is the primary way to create new resolvers. It has some sugars
to reduce the boilerplate code related to define a new resolver.
The main pieces of a resolver are:
::pco/op-name- a symbol that identify the resolver, like a function name.
::pco/output- a EQL expression that informs the attributes provided by this resolver.
::pco/input- a EQL expression that informs the attributes required by this resolver.
Both input and output can express flat or nested queries, you should always strive to be as precise with these descriptions as you can (matching the shapes of your inputs and outputs), Pathom leverages this information in many ways during transaction processing, and as more accurate you get, the more Pathom can do with it.
Let's start with a verbose usage of
defresolver to go over these main pieces:
This resolver expresses an edge in Pathom attribute world. It says the attribute
is reachable, given that a
:acme.article/title is available.
If you look close, you can find a fair amount of repetition in this expression:
op-nameis repeated at the start to define the
inputis repeated in the destructuring expression.
outputis visible in the output expression.
Experience with Pathom 2 demonstrated these repetitions occur frequently in most Pathom code bases. So Pathom 3 adds some extra sugar tricks to avoid the repetition:
- We can remove the
defresolveruse the same symbol as the
If you use Cursive, you can ask it to resolve the
pco/defresolver as a
defn, and you will get proper symbol resolution
- Now, given the
inputis the same as destructuring, we can let
defresolverinfer the input from it:
You can also use destructuring via symbol binding, for example, this works the same:
- Since the last element of our expression is a map,
defresolvercan infer the
Pathom only looks at the last item of the body for implicit outputs. This means that
if you wrap the map with something like a
let statement, Pathom will not be able
to figure it out. Instead, you can move the
let down on the attribute level:
In the future, Pathom may get smarter around this, but this is a long rabbit role when
I consider that the last user statement may be an
Although Pathom can infer the configuration, you can always override it using the options map. In case of inputs and params, your explicit definitions will be merged with the inferred ones from Pathom, giving precedence to the explicit definition in case of conflicts.
- Given we are not using
env, we can also take it out, taking us to the final version:
In case you have a resolver that doesn't use an input, you can omit the first argument as well, for example:
Like functions, you can call the resolvers directly:
This makes it easier to test resolvers in unit tests.
A direct call uses the arity of the resolver. In this example, the resolver takes a single parameter
representing a map containing the key
Outside of the test context, you should have little reason to invoke a resolver directly. What sets resolvers apart from regular functions are that it contains data about its requirements and what it provides.
This allows you to abstract away the operation names and focus only on the attributes.
To illustrate the difference, let's compare the two approaches, first let's define the resolvers:
Consider the question: What is the
avatar url of the user with id
The traditional way to get there using our resolvers as functions goes as:
Now check a few examples of how we can accomplish the same using the a smart map leveraged by the resolvers meta-data:
Note how as we change the input, Pathom uses different resolvers to satisfy the request for data. In this way, Pathom allow you to request the data you need, in a declarative fashion, rather than making procedural calls. It removes the chore of knowing the function names, so you can focus on the data you have and need, instead of how to get it.
Using the indexes
Here we will cover in more detail the process to generate the indexes from resolvers.
Think of resolvers as the atomic blocks of Pathom, which are annotated functions. If Pathom had to scan every resolver to figure a path, it would be slow. So instead, Pathom works on indexes generated from a set of resolvers.
To generate the indexes, we use the function
(pci/register ...). The values allowed
- sequence of previous items (recursively)
Here are a couple of examples to illustrate:
To learn more details about the indexes, check the indexes page.
indexes at hand you can leverage the Pathom engine to call the resolvers for you.
indexes are part of the Pathom environment, for some operations just the indexes
are enough. When you see
env, consider they expect to have at least
the indexes for the resolvers in the map.
You can declare to Pathom that a resolver input is optional. To do so, you can use the
pco/? around the attribute.
In practice, this helper adds an EQL param to the keyword, marking it as optional.
For this example you can see a resolver that tells how to call the user (for some gretting
message on a site for example). We call this attribute
:user/display-name. All users
have emails, but not all users have their names in our database, so to create the
:user/display-name we make the
:user/name attribute optional:
Since Pathom 3
2022.10.18-alpha you can also use the
:or section of the map destructing
to inform Pathom about optionals:
Pathom supports requiring nested inputs on a resolver. This feature is handy when you need to make data aggregation based on indirect data.
To illustrate this, consider the following setup:
Now, for the task: how to make a resolver that computes the average score from the top players?
If you use the flat inputs as we did before and ask for
:game/top-players it will
contain the ids, but not the scores. To make the scores available, a nested processing
is required in this case, and this where nested inputs come to rescue:
When Pathom is planning, it will verify if a sub-query part reachable. If there is no available path to fulfill the nested part, the planner will discard that path, avoiding running unnecessary work on the runner.
You can try this by asking for a nested property that doesn't exist.
Parameters are a secondary layer to send information to a resolver. One main difference between params and inputs is that params do not participate in the graph traversal; they come as-is.
You can use parameters to allow the user to configure the request for some attribute.
To illustrate, lets start with a resolver that pulls todo from some mock memory data:
We are using explicit output in the
todos-resolver so Pathom has more information about
the structured shape. Although the resolver works without the nested specification, having
it makes it possible for tools to auto-complete better, and this is important when
trying to integrate Pathom indexes, so as a general practice, when resolvers have nested
data, it's better always to specify it.
To add params to a query attribute, we can use EQL params, for example:
If we just ran the previous code, nothing happens. To make this work, we need to use the parameters to filter the data in the resolver code.
To pull the params we can use the helper
(pco/params env), then filter the list based
To use parameters with Smart Maps you need to call
looking up the key, for example:
Example implementation using a more generic function to filter data:
It's possible to send a function to transform the resolver before its instantiation.
This allows to create some helpers that you may want to apply only to some specific resolvers.
As an example, let's make a transform function that transforms a single resolver in a batch resolver:
The transform function receives the configuration map of the resolver (including the
execution function, as
::pco/resolve). Then Pathom uses the returned map to instantiate
Batch in Pathom is the process of calling a resolver once with a batch of inputs. So instead of running the resolver once for each item, it runs the resolver one time with a sequence of inputs.
Another way to put it: it's how Pathom deals with the N+1 problem.
Here is a resolver that simulates a delay, let's run in a list and check the time:
As we can see, the ~900ms is expected since the resolver ran once for each item.
Consider a case where some API has a batching feature; in this case, would be ideal to reduce the number of round trips, you can do it by writing a batch resolver. The main the difference is that, instead of an input map, you will get a sequence of maps, with all the inputs collected for batching. Use this to return a list (of the same input size) and Pathom does the merging in place:
It's important that the output sequence of a batch resolver has the size and order as the input. Sometimes API's return things out of order, sometimes it misses items. To see an example, let's shuffle the order of our batch results and see what happens to the results:
Another thing that can ruin the matching is if there are missing records, but if that happens Pathom will trigger an error
To fix these problems you can use the helper
In the following example we alias
[com.wsscode.misc.coll :as coll].
Batch in Parallel processor
Batching works a bit differently in the parallel processor.
In parallel execution we don't have a clear signal that we finished a scan on the query.
For that reason we need another way to decide we accumulated enough and trigger the batch request.
The strategy in the parallel case is based on two configurations:
::pcrp/batch-hold-delay-ms - After not receiving a new entry for an amount of milliseconds,
Pathom will fire the batch with the items accumulated so far.
::pcrp/batch-hold-flush-threshold - When the accumulator reaches the threshold, it
triggers the batch immediately.
Batch in chunks
Since Pathom 3
2022.03.04-alpha, the batch mode supports defining a chunk size.
This is specially useful when dealing with large data sets where the input size might go over some limit on the external system:
In the previous setup, now if we have for example 25 items, instead of a single call with 25 items, we will get 2 calls with 10 items and one with 5 items.
Not all paths are supported in batch, for example:
In cases like these, you will a warning telling you that batching is unsupported. The reason for this behavior is due to how batching works. Pathom collects entries for batch, and they run all together at the root entity context. Once the results are available, Pathom has to inject the response back into the tree.
The process works fine with maps and vectors but not with sequences, lists, or sets. That's because the later data structures don't support indexed updates for the elements in them.
When Pathom knows it is such path, it won't try to batch. It runs the resolver using a single collection element and gets the first result. This is effective the same as using the resolver without batching.
If you see the warning, check the tree and find the collection that's isn't a vector and then you can fix it.
Don't overuse batch
Don't batch when the resolver operation is quick. If there is no IO in the resolver, consider keeping it non-batched. Also, if you are using SQLite, you don't need to batch at all.
There are cases where multiple paths are available to get the same data.
You can use the attribute
::pco/priority to indicate to Pathom that a resolver is
preferred. The value of
::pco/priority is an integer. If not set, the default value
is zero. Negative values are allowed.
This method of prioritization still experimental and subject to change, join the discussion at https://github.com/wilkerlucio/pathom3/discussions/57.
To demonstrate the usage of priority, the following example shows two versions to get the name, one from cache (preferred), but falling back to the standard db one when the cache is unable to deliver.
You can see that the cached resolver returns
::pco/unknown-value. This is equivalent to not having
that key in the response. Doing this way, we can leverage the output inference while expressing this value is unknown, which triggers the next option.
It's important to notice that Pathom will choose will depend on the highest priority resolver on that path. This means a dependency with a high priority may override the priority of an "end resolver".
Here is an example of this scenario:
Here is the graph to help visualize it:
Custom priority algorithm
If you need fine control over prioritization, you can set the key
in the env, to provide your custom function.
To understand how to make a custom priority function, you must first understand
how the Pathom planner works. Especially the part on
OR nodes, you can find it here.
This is the default
It takes an environment (which has the graph inside at
in process and a set of
node-ids, from which you should pick one.
OR node branches could be as simple as two resolvers to choose
from. In other cases, it can be huge, with a complex run graph underneath. So you must
consider this if you are writing a custom prioritization function.
To illustrate this, let's see how Pathom implements
priority-sort. The algorithm traverses each of the
OR branches to find their leaf nodes for priority sort. This way
we compare the edges (which does what you would expect).
After comparing the edges, we need to return the original branch node, so we store it as part of the data.
Some leaf nodes may be other branch nodes. In those cases, we run the prioritization again against those nodes, and take the winner to compare also with the parent items.
To finish up, compare the now generates leaves and take the winner path.
When you override
::pcr/choose-path, you can start by copying the
you saw before here, and adjust it for your needs.
You can also create a resolver using the
resolver helper. This is more useful when
you are making some helper function to generate a resolver.
To create a resolver using
pco/resolver you have the following options:
As an example, here are how to generate resolvers to create unit conversion between meters and foot to a given attribute name:
The environment is a map containing contextual information about the process. You can find information about environment details at the environment page.