Connect mutations

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

Mutations setup

The mutation setup looks very much like the one for resolvers, you define them using pc/defmutation and include them in the registry, just like the resolvers.

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

(pc/defmutation my-mutation [env params] ...)

(def parser
  (p/parser
    {::p/env     {::p/reader [p/map-reader
                              pc/reader2
                              pc/open-ident-reader]}
     ::p/mutate  pc/mutate
     ::p/plugins [(pc/connect-plugin {::pc/register my-mutation})
                  p/error-handler-plugin
                  p/trace-plugin]}))

Now let’s write a mutation with our factory.

Creating mutations

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

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

(pc/defmutation send-message [env {:keys [message/text]}]
  {::pc/sym    'send-message
   ::pc/params [:message/text]
   ::pc/output [:message/id :message/text]}
  {:message/id   123
   :message/text text})

(def parser
  (p/parser
    {::p/env     {::p/reader [p/map-reader
                              pc/reader2
                              pc/open-ident-reader]}
     ::p/mutate  pc/mutate
     ::p/plugins [(pc/connect-plugin {::pc/register send-message})
                  p/error-handler-plugin
                  p/trace-plugin]}))
[(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’s 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.

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

(pc/defmutation create-user [{::keys [db]} user]
  {::pc/sym    'user/create
   ::pc/params [:user/name :user/email]
   ::pc/output [:user/id]}
  (let [{:keys [user/id] :as new-user}
        (-> user
            (select-keys [:user/name :user/email])
            (merge {:user/id         (random-uuid)
                    :user/created-at (js/Date.)}))]
    (swap! db assoc-in [:users id] new-user)
    {:user/id id}))

(pc/defresolver user-data [{::keys [db]} {:keys [user/id]}]
  {::pc/input  #{:user/id}
   ::pc/output [:user/id :user/name :user/email :user/created-at]}
  (get-in @db [:users id]))

(pc/defresolver all-users [{::keys [db]} _]
  {::pc/output [{:user/all [:user/id :user/name :user/email :user/created-at]}]}
  {:user/all (vals (get @db :users))})

(def api-registry [create-user user-data all-users])

(def parser
  (p/parser
    {::p/env     {::p/reader [p/map-reader pc/reader2 pc/open-ident-reader]
                  ::db       (atom {})}
     ::p/mutate  pc/mutate
     ::p/plugins [(pc/connect-plugin {::pc/register api-registry})
                  p/error-handler-plugin
                  p/trace-plugin]}))
[{(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:

(ns com.wsscode.pathom.book.connect.mutation-join-globals
  (:require [com.wsscode.pathom.connect :as pc]
            [com.wsscode.pathom.core :as p]))

(pc/defmutation user-create [{::keys [db]} user]
  {::pc/sym    'user/create
   ::pc/params [:user/name :user/email]
   ::pc/output [:user/id]}
  (let [{:keys [user/id] :as new-user}
        (-> user
            (select-keys [:user/name :user/email])
            (merge {:user/id         (random-uuid)
                    :user/created-at (js/Date.)}))]
    (swap! db assoc-in [:users id] new-user)
    {:user/id       id
     :app/id-remaps {(:user/id user) id}}))

(pc/defresolver user-data [{::keys [db]} {:keys [user/id]}]
  {::pc/input  #{:user/id}
   ::pc/output [:user/id :user/name :user/email :user/created-at]}
  (get-in @db [:users id]))

(pc/defresolver all-users [{::keys [db]} _]
  {::pc/output [{:user/all [:user/id :user/name :user/email :user/created-at]}]}
  {:user/all (vals (get @db :users))})

(def app-registry [user-create user-data all-users])

(def parser
  (p/parser
    {::p/env     {::p/reader                 [p/map-reader
                                              pc/reader2
                                              pc/open-ident-reader
                                              p/env-placeholder-reader]
                  ::pc/mutation-join-globals [:app/id-remaps]
                  ::p/placeholder-prefixes   #{">"}
                  ::db                       (atom {})}
     ::p/mutate  pc/mutate
     ::p/plugins [(pc/connect-plugin {::pc/register app-registry})
                  p/error-handler-plugin
                  p/trace-plugin]}))
[{(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 :com.fulcrologic.fulcro.algorithms.tempid/tempid as a global and have that pass through.

Mutation output context

Mutation context allows the mutation caller to provide extra data to be used as context information for further processing in the mutation response.

During UI development, sometimes you may want to load some data in response to the mutation but the mutation output doesn’t have enough context, although the UI does (because it has a much bigger view at the client data). For those cases the UI can send some params to the mutation so those are available for traversing in the mutation response.

To demonstrate this, check the following example:

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

(pc/defmutation create-user [{::keys [db]} user]
  {::pc/sym    'user/create
   ::pc/params [:user/name :user/email]
   ::pc/output [:user/id]}
  (let [{:keys [user/id] :as new-user}
        (-> user
            (select-keys [:user/name :user/email])
            (merge {:user/id         (random-uuid)
                    :user/created-at (js/Date.)}))]
    (swap! db assoc-in [:users id] new-user)
    {:user/id id}))

(pc/defresolver user-data [{::keys [db]} {:keys [user/id]}]
  {::pc/input  #{:user/id}
   ::pc/output [:user/id :user/name :user/email :user/created-at]}
  (get-in @db [:users id]))

(pc/defresolver all-users [{::keys [db]} _]
  {::pc/output [{:user/all [:user/id :user/name :user/email :user/created-at]}]}
  {:user/all (vals (get @db :users))})

(pc/defresolver n++ [_ {:keys [number/value]}]
  {::pc/input  #{:number/value}
   ::pc/output [:number/value++]}
  {:number/value++ (inc value)})

(def api-registry [create-user user-data all-users n++])

(def parser
  (p/parser
    {::p/env     {::p/reader [p/map-reader pc/reader2 pc/open-ident-reader]
                  ::db       (atom {})}
     ::p/mutate  pc/mutate
     ::p/plugins [(pc/connect-plugin {::pc/register api-registry})
                  p/error-handler-plugin
                  p/trace-plugin]}))
[{(user/create {:user/id "TMP_ID" :user/name "Rick Sanches" :user/email "rick@morty.com" :pathom/context {:number/value 123}}) [:number/value :number/value++]}]

One real use-case for this feature would be in a Fulcro app, when you send some mutation but the result needs to update some component elsewhere (and the required data is known by the client, but not by the original mutation result).

Async mutations

If you use async-parser or parallel-parser you must use the mutate-async so mutations work and support returning channels.

Here is an example of doing some mutation operations using async features.

Example:

(ns com.wsscode.pathom.book.connect.mutation-async
  (:require [cljs.core.async :as async :refer [go]]
            [com.wsscode.common.async-cljs :refer [go-catch <!p <?]]
            [com.wsscode.pathom.book.util.indexeddb :as db]
            [com.wsscode.pathom.connect :as pc]
            [com.wsscode.pathom.core :as p]))

(defn adapt-user [user]
  (-> (into {} (map (fn [[k v]] [(keyword "user" (name k)) v])) (dissoc user ::db/key))
      (assoc :user/id (::db/key user))))

(pc/defmutation user-create [{::keys [db]} user]
  {::pc/sym    'user/create
   ::pc/params [:user/name :user/email]
   ::pc/output [:user/id]}
  (go
    (let [db      (<? db)
          user-id (-> user
                      (select-keys [:user/name :user/email])
                      (merge {:user/created-at (js/Date.)})
                      (->> (db/create! {::db/db db ::db/store-name "users"}))
                      <?)]
      {:user/id       user-id
       :app/id-remaps {(:user/id user) user-id}})))

(pc/defresolver user-by-id [{::keys [db]} {:keys [user/id]}]
  {::pc/input  #{:user/id}
   ::pc/output [:user/id :user/name :user/email :user/created-at]}
  (go
    ; reading from indexeddb
    (let [db (<? db)]
      (-> (db/read-object {::db/db db ::db/store-name "users"} id) <?
          adapt-user))))

; let's make an access to all users
(pc/defresolver all-users [{::keys [db]} _]
  {::pc/output [{:user/all [:user/id :user/name :user/email :user/created-at]}]}
  (go
    (let [db (<? db)]
      (->> (db/scan-store {::db/db db ::db/store-name "users"})
           (async/into []) <?
           (mapv adapt-user)
           (hash-map :user/all)))))

; list all our app resolvers and mutations
(def app-registry [user-create user-by-id all-users])

(def db-settings
  {::db/db-name    "connectAsyncDemo"
   ::db/migrations [{::db/stores {"users" {::db/keys    ::db/auto-increment
                                           ::db/indexes {"name" {::db/unique false}}}}}]})

(def parser
  (p/parallel-parser
    {::p/env     {::p/reader               [p/map-reader
                                            pc/parallel-reader
                                            pc/open-ident-reader
                                            p/env-placeholder-reader]
                  ::p/placeholder-prefixes #{">"}
                  ::pc/mutation-join-globals [:app/id-remaps]
                  ::db                       (db/setup-db db-settings)}
     ::p/mutate  pc/mutate-async
     ::p/plugins [(pc/connect-plugin {::pc/register app-registry})
                  p/error-handler-plugin
                  p/trace-plugin]}))
[{(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 an indexedDB database.

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