Plugins
Plugins provide a way to extend Pathom behavior. This page explains how Pathom plugins work and how you can write your extensions.
The wrapping model of plugins
Pathom extensions work by wrapping
code points in Pathom. This means you can code
around the extended points in Pathom.
Each extension point has an identifier keyword.
Let's take for example the :com.wsscode.pathom3.format.eql/wrap-map-select-entry
extension. This adds a hook to the result masking operation, which occurs at the
end of p.eql/process
to filter out the results according to the query.
It's a Pathom convention to prefix all plugin wrapping attributes with wrap-
.
This hook, in specific, can control how the value is placed at the resulting map.
Let's use this hook to make an extension that protects certain attributes from getting in the output map:
(ns com.wsscode.pathom3.docs.demos.core.plugins
(:require [com.wsscode.misc.coll :as coll]
[com.wsscode.pathom3.connect.built-in.resolvers :as pbir]
[com.wsscode.pathom3.connect.indexes :as pci]
[com.wsscode.pathom3.interface.eql :as p.eql]
[com.wsscode.pathom3.plugin :as p.plugin]))
(def protected-attributes
#{:user/password})
; define the extension wrapper fn
(defn protect-attributes-wrapper [mse]
(fn [env source {:keys [key] :as ast}]
(if (and (contains? source key)
(contains? protected-attributes key))
; the output of this extension must be a map entry or nil
; a vector with two elements would also work, but creating a map entry is
; more efficient
(coll/make-map-entry key ::protected-value)
(mse env source ast))))
; create the plugin
(p.plugin/defplugin protect-attributes-plugin
{:com.wsscode.pathom3.format.eql/wrap-map-select-entry
protect-attributes-wrapper})
(def users
{1 {:user/id 1
:user/name "User Name"
:user/password "12345"}})
(def env
(-> (p.plugin/register protect-attributes-plugin)
(pci/register [(pbir/static-table-resolver :user/id users)])))
(p.eql/process env
'[{[:user/id 1]
[:user/name :user/password]}])
; => {[:user/id 1]
; {:user/name "User Name",
; :user/password ::protected-value}}
The data from those attributes may still leak in the meta for stats, to make it really secure you need to prevent regular users from seeing the stats.
Now let's talk in more detail about the things we used in this example.
Define the wrapper fn
A more generic way to think about wrapper extensions is with the following structure:
(defn wrapper-fn [original-fn]
; the number of args in the internal-impl depends on which function the plugin is wrapping
(fn internal-impl [args]
; here we are wrapping the original fn, we can both alter its arguments or the return
; of it, you can think of the code before the original-fn as the ENTER part, and
; the code after (possibly modifying the output) as the LEAVE part.
(original-fn args)))
Define the plugin
Then, a plugin consists of one or more of those wrapping functions. Any map can be a
plugin, to make it a plugin, you must add the key ::p.plugin/id
with a symbol to
identify this plugin. Then each other key is an extension wrapper.
; using defplugin it defines the var and adds the id to the map
(p.plugin/defplugin protect-attributes-plugin
{:com.wsscode.pathom3.format.eql/wrap-map-select-entry
my-wrapper})
; this is how to do the same thing without the macro:
(def protect-attributes-plugin
{::p.plugin/id
`protect-attributes-plugin
:com.wsscode.pathom3.format.eql/wrap-map-select-entry
my-wrapper})
The latter form is interesting when you are generating a plugin programmatically.
For example, we could make our plugin to protect attributes more configurable by using making a function that returns the plugin:
(defn protect-attributes-plugin [protected-attributes]
{::p.plugin/id
`protect-attributes-plugin
:com.wsscode.pathom3.format.eql/wrap-map-select-entry
(fn [mst]
(fn [env source {:keys [key] :as ast}]
(if (and (contains? source key)
(contains? protected-attributes key))
; the output of this extension must be a map entry or nil
; a vector with two elements would also work, but creating a map entry is
; more efficient
(coll/make-map-entry key ::protected-value)
(mst env source ast))))})
(def env
; create plugin to protect specific attributes and add it
(-> (p.plugin/register (protect-attributes-plugin #{:user/password}))
(pci/register [(pbir/static-table-resolver :user/id users)])))
Adding plugins
To use a plugin, you must add it to the environment using the p.plugin/register
function.
This function works similar to pci/register
and also accepts vectors
of plugins.
In the case of plugins, there is also p.plugin/register-before
and
p.plugin/register-after
. Which gives you more control over the plugin execution order.
In the next section, we will talk more about plugin order.
Plugin order
To understand the order in which plugins execute, it helps to keep the following image in your head:
Here is the same description as code:
; register in a single call
(p.plugin/register
[plugin-a
plugin-b
plugin-c])
; in many calls, same result:
(-> {}
(p.plugin/register plugin-a)
(p.plugin/register plugin-b)
(p.plugin/register plugin-c))
Plugins on top will enter first and leave last.
You can view the plugin order in the attribute ::p.plugin/plugin-order
attribute in
your environment.
A plugin can halt the execution of the posterior plugins (and the original operation itself). This happens when the plugin returns some data without calling the original function given on the wrapper.
For example, if plugin-b
halts execution (like our protected attributes plugin does)
the stack stops there and goes back up. The following image illustrates this case:
Consider this when deciding the order of plugins.
When you use p.plugin/register
, Pathom creates specific lists for each plugin type. This way, it only has to iterate over plugins of that extension type.
Extension points
Here you can see all the extension points available in Pathom.
Runner Extensions
Extensions available for the runner process of Pathom at com.wsscode.pathom3.connect.runner
namespace.
::pcr/wrap-resolve
Wrap the call of a resolver. Note in the case of a batch resolver, input is going to be a sequence.
(defn sample-resolve-wrapper [resolve]
(fn [env input]
(resolve env input)))
::pcr/wrap-mutate
Wrap the call of a mutation.
(defn sample-mutate-wrapper [mutate]
(fn [env ast]
(mutate env ast)))
::p.error/wrap-attribute-error
Wrap the error data, allowing you to modify it.
(defn sample-attribute-error-wrapper [attr-error]
(fn [entity k]
(let [err (attr-error entity k)]
; will return just the error cause
(:com.wsscode.pathom3.error/cause err))))
::pcr/wrap-resolver-error
Wrap the operation that attaches an error to a node.
(defn sample-resolver-error-wrapper [resolver-error]
(fn [env ast error]
(let [e (resolver-error env ast error)]
; you may modify the error here
e)))
::pcr/wrap-batch-resolver-error
Wrap batch errors
(defn sample-batch-resolver-error-wrapper [_]
(fn [env [batch-op batch-items] e]
; report error
))
::pcr/wrap-mutation-error
Use this to get notified when mutation errors happen.
(defn sample-mutation-error-wrapper [mutation-error]
(fn [env ast error]
(let [e (mutation-error env ast error)]
; you may modify the error here
e)))
::pcr/wrap-merge-attribute
Wrap the operation of merging new data in the entity. This is an important point. You
can control how sub-processes occur here. The source original is an assoc
like operation.
(defn sample-merge-attribute-wrapper [original]
(fn [env out k v]
(original env out k v)))
::pcr/wrap-entity-ready!
This runs only once per entity, if you want to do any kind of post-process that you expect one entity to be ready.
(defn sample-run-graph-wrapper [original]
(fn [env]
(original env)))
::pcr/wrap-run-graph!
Wrap the operation of running a graph. Note for a given transaction, there may be many graphs to run. Each entity (each map in the result) is potentially an entity and may have its own graph.
Also note this may run multiple times for the same entity, for each "batch hit" it does.
(defn sample-run-graph-wrapper [original]
(fn [env ast-or-graph entity-tree*]
(original env ast-or-graph entity-tree*)))
::pcr/wrap-root-run-graph!
Like the previous, but only runs at the root graph.
EQL Output
::pf.eql/wrap-map-select-entry
This is the one we used for the initial demo. Controls how outputs get masked out in the post-processing of the EQL output.
(defn sample-map-select-entry-wrapper [original]
(fn [env source ast]
(original env source ast)))
::p.eql/wrap-process-ast
This wraps a call to process-ast
in the EQL interface.
(defn sample-process-ast-wrapper [process]
(fn [env ast]
(process env ast)))
When the query is triggered using process
, you also get the query as ::pcr/root-query
in the environment.
If you don't see this value it means process-ast
was called directly. To get the query
in this case use eql/ast->query
to generate the query from the AST.