Fulcro Integration

There are two main ways in which to use Pathom + GraphQL + Fulcro:

  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.graphql2 :as pcg]
[com.wsscode.pathom.fulcro.network :as pfn]
Before Pathom 2.2.12 the default functions to work with GraphQL used to convert the standard Clojure hyphenated to GraphQL camel case format, but after some user reports we realized that wasn’t a good idea because some names could never be accessed when entry points started with capital letters. To avoid those problems, since Pathom 2.2.12 we recommend new implementations that don’t transform the names in any way by default, but at same time provides custom name munging if the user wants to use it. None of the previous code was changed so library clients will not break with this change, we are just using new namespaces that use the new simpler way.

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-network2
       {::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 will be used to send to GraphQL

Mutations on a Simple GraphQL remote have the following rules:

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

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         [{:allTodoItems (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 :allTodoItems todo/TodoItem {:target [::root "singleton" :allTodoItems]}))

  :networking
  {:remote (pfn/graphql-network2 "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 createTodoItem [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.

(ns com.wsscode.pathom.workspaces.graphql.simple-todo-demo
  (:require [fulcro.client.primitives :as fp]
            [nubank.workspaces.card-types.fulcro :as ct.fulcro]
            [nubank.workspaces.lib.fulcro-portal :as f.portal]
            [nubank.workspaces.core :as ws]
            [fulcro.client.data-fetch :as df]
            [com.wsscode.pathom.fulcro.network :as pfn]
            [fulcro.client.mutations :as fm]
            [com.wsscode.fulcro.db-helpers :as db.h]
            [com.wsscode.fulcro.ui.reakit :as rk]
            [com.wsscode.fulcro.ui.icons.font-awesome :as fa]))

(declare TodoItem)

(fm/defmutation updateTodoItem [todo]
  (action [{:keys [state ref]}]
    (swap! state update-in ref merge todo))
  (remote [{:keys [ast state]}]
    (-> ast
        (fm/returning state TodoItem))))

(fm/defmutation deleteTodoItem [{:todo/keys [id]}]
  (action [env]
    (db.h/swap-entity! env update :allTodoItems #(into [] (remove (comp #{id} second)) %)))
  (remote [{:keys [ast state]}]
    (-> ast
        (update :params select-keys [:todo/id])
        (fm/returning state TodoItem))))

(fp/defsc TodoItem
  [this {:todo/keys [id title completed]} {::keys [on-delete-todo]}]
  {:initial-state (fn [_]
                    {:todo/id        (fp/tempid)
                     :todo/title     ""
                     :todo/completed false})
   :ident         [:todo/id :todo/id]
   :query         [:todo/id :todo/title :todo/completed]
   :css           [[:.completed [:label {:text-decoration "line-through"}]]
                   [:.creating {:color "#ccc"}]]
   :css-include   []}
  (rk/flex {:classes    [(if completed :.completed)
                         (if (fp/tempid? id) :.creating)]
            :alignItems "center"}
    (rk/label
      (rk/input {:type        "checkbox"
                 :checked     completed
                 :marginRight 5
                 :onChange    #(fp/transact! this [`(updateTodoItem ~{:todo/id id :todo/completed (not completed)})])})
      (str title))
    (rk/inline-block {:cursor  "pointer"
                      :onClick on-delete-todo}
      (fa/close))))

(def todo-item (fp/factory TodoItem {:keyfn :todo/id}))

(fp/defsc NewTodo
  [this {:todo/keys [title]} {::keys [on-save-todo]}]
  {:initial-state (fn [_]
                    {:todo/id        (fp/tempid)
                     :todo/title     ""
                     :todo/completed false})
   :ident         [:todo/id :todo/id]
   :query         [:todo/id :todo/title :todo/completed]
   :css           []
   :css-include   []}
  (rk/group {:marginBottom 10}
    (with-redefs [fulcro.client.dom/form-elements? (fn [_] false)]
      (rk/input {:type     "text"
                 :value    title
                 :onChange #(let [value (.. % -target -value)] (fm/set-value! this :todo/title value))}))
    (rk/button {:onClick #(on-save-todo (fp/props this))}
      "Add"
      (fa/plus-square))))

(fm/defmutation createTodoItem [todo]
  (action [env]
    (db.h/swap-entity! env update :allTodoItems conj (fp/get-ident TodoItem todo))
    (db.h/create-entity! env NewTodo {} :replace :ui/new-todo))
  (remote [{:keys [ast]}]
    (update ast :params select-keys [:todo/id :todo/title])))

(def new-todo-ui (fp/factory NewTodo {:keyfn :todo/id}))

(fp/defsc TodoSimpleDemo
  [this {:keys [allTodoItems] :ui/keys [new-todo]}]
  {:initial-state (fn [_]
                    {:ui/new-todo (fp/get-initial-state NewTodo {})})
   :ident         (fn [] [::root "singleton"])
   :query         [{:ui/new-todo (fp/get-query NewTodo)}
                   {:allTodoItems (fp/get-query TodoItem)}]
   :css           []
   :css-include   [TodoItem NewTodo]}
  (rk/block
    (new-todo-ui (fp/computed new-todo {::on-save-todo #(fp/transact! this [`(createTodoItem ~%)])}))
    (for [todo allTodoItems]
      (todo-item (fp/computed todo {::on-delete-todo #(fp/transact! this [`(deleteTodoItem ~todo)])})))))

(ws/defcard todo-simple-demo
  (ct.fulcro/fulcro-card
    {::f.portal/root TodoSimpleDemo
     ::f.portal/app  {:started-callback
                      (fn [app]
                        (df/load app :allTodoItems TodoItem {:target [::root "singleton" :allTodoItems]}))

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

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.

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 interfacing with GitHub: we might choose the prefix github. Then our keywords would need to be things like :github.User/name.

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

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/createdAt]}]

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.

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, that’s because the GraphQL schema will be loaded asynchronosly later and we need the index reference to add the GraphQL connection.

(defonce indexes (atom {}))

We need to define the configuration for the GraphQL connection:

(def github-gql
  {::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.Repository/name}}
   ::p.http/driver p.http.fetch/request-async})
::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).

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 a parser. This will essentially be basically this:

(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 #{">"}
                  ::p.http/driver          p.http.fetch/request-async}
     ::p/mutate  pc/mutate-async
     ::p/plugins [(pc/connect-plugin {; we can specify the index for the connect plugin to use
                                      ; instead of creating a new one internally
                                      ::pc/indexes  indexes})
                  p/error-handler-plugin
                  p/trace-plugin]}))

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

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!

(pc/defresolver repositories [_ _]
  {::pc/output [{:demo-repos [:github.User/login :github.Repository/name]}]}
  {: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!

Remember to add this resolver definition before the parser, then we have to add this resolver to our connect system, do that by updating the call to the connect-plugin, here is the updated parser:

(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 #{">"}
                  ::p.http/driver          p.http.fetch/request-async}
     ::p/mutate  pc/mutate-async
     ::p/plugins [(pc/connect-plugin {::pc/register repositories ; registering the resolver
                                      ::pc/indexes  indexes})
                  p/error-handler-plugin
                  p/trace-plugin]}))

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

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 nameWithOwner viewerHasStarred]}]
  {:ident [:github.Repository/id :github.Repository/id]
   :query [:github.Repository/id :github.Repository/nameWithOwner :github.Repository/viewerHasStarred]}
  ...)

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

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

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/addStar [_]
  (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.

Connect-Based Mutations

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.

The notation is the same as for resolvers:

(pc/defmutation custom-mutation [_ params]
  {::pc/sym 'custom-mutation         ;; (optional) if provided will be used as mutation symbol, otherwise it will use the def symbol (including namespace)
   ::pc/params [:id {:boo [:y]}]     ;; future autocomplete...noop now
   ::pc/output [:x]}                 ;; future autocomplete...
  ;; 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 body 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, then you’ll also have to make sure they’re OK with the mutation symbolically (e.g. define a fm/defmutation as well).

Multiple Input Entry Points

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.

Since version 2.2.0 the connect readers ident-reader and open-ident-reader support the provision of extra context information using the query parameter :pathom/context.

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}) [...]}]

Here is how you can use it to query for a pathom in the Github GraphQL API:

[{([:github.repository/name "pathom"] {:pathom/context {:github.repository/owner "wilkerlucio"}}) [...]}]

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] {:pathom/context {:github.user/login owner}}))

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

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

and we’re good to go!

Customizing Result Parsing

Under the hood, Pathom uses a parser reader to do some error handling and bookkeeping on the query result. The simplest way to customize query results is to pass in custom mung and demung functions. These can be added as optional keys to the GraphQL configuration map. For example, if our EQL query keywords are in kebab case, but the GraphQL schema uses camel case, we can make the Connect plugin do the conversion for us with the following configuration:

(def github-gql
  {::pcg/url       (str "https://api.github.com/graphql?access_token=" (ls/get :github-token))
   ::pcg/prefix    "github"
   ::pcg/mung      pg/kebab-case
   ::pcg/demung    pg/camel-case
   ::pcg/ident-map {"user"       {"login" :github.User/login}
                    "repository" {"owner" :github.User/login
                                  "name"  :github.Repository/name}}
   ::p.http/driver p.http.fetch/request-async})

We can completely customize the query results by passing our own custom parser. See pcg/parser-item as an example of what such a parser should look like. This could be used to coerce uuid values from strings to uuids. Here’s an example of adapting pcg/parser-item to also coerce :my.gql.item/id values to uuids:

(defn demunger-map-reader
  "Reader that will demunge keys and coerce :my.gql.item/id values to uuids"
  [{::keys [demung]
    :keys  [ast query]
    :as    env}]
  (let [entity (p/entity env)
        k (:key ast)]
    (if-let [[_ v] (find entity (pcg/demung-key demung k))]
      (do
        (if (sequential? v)
          (if query
            (p/join-seq env v)
            (if (= k :my.gql.item/id)
              (map uuid v)
              v))
          (if (and (map? v) query)
            (p/join v env)
            (if (= k :my.gql.item/id)
              (uuid v)
              v))))
      ::p/continue)))

(def parser-item
  (p/parser {::p/env     {::p/reader [pcg/error-stamper
                                      demunger-map-reader
                                      p/env-placeholder-reader
                                      pcg/gql-ident-reader]}
             ::p/plugins [(p/env-wrap-plugin
                           (fn [env]
                             (-> (merge {::demung identity} env)
                                 (update ::p/placeholder-prefixes
                                         #(or % #{})))))]}))

(def my-gql-config
  {::pcg/url         "https://api.mydomain.com/graphql"
   ::pcg/prefix      "my.gql"
   ::pcg/parser-item parser-item
   ::pcg/ident-map   {"item" {"id" :my.gql.item/id}}
   ::p.http/driver   p.http.fetch/request-async})

This is only lightly edited from the implementation of pcg/parser-item.

Complete GraphQL Connect Example

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

(ns com.wsscode.pathom.workspaces.graphql.github-demo
  (:require
    [com.wsscode.common.async-cljs :refer [go-promise let-chan <!p go-catch <? <?maybe]]
    [com.wsscode.pathom.book.util.local-storage :as ls]
    [com.wsscode.pathom.connect :as pc]
    [com.wsscode.pathom.connect.graphql2 :as pcg]
    [com.wsscode.pathom.core :as p]
    [com.wsscode.pathom.diplomat.http :as p.http]
    [com.wsscode.pathom.diplomat.http.fetch :as p.http.fetch]
    [com.wsscode.pathom.fulcro.network :as pfn]
    [com.wsscode.pathom.viz.query-editor :as pv.query-editor]
    [com.wsscode.pathom.viz.workspaces :as pv.ws]
    [fulcro.client.data-fetch :as df]
    [fulcro.client.localized-dom :as dom]
    [fulcro.client.mutations :as fm]
    [fulcro.client.primitives :as fp]
    [nubank.workspaces.card-types.fulcro :as ct.fulcro]
    [nubank.workspaces.core :as ws]
    [nubank.workspaces.lib.fulcro-portal :as f.portal]))

(defonce indexes (atom {}))

(pc/defresolver repositories [_ _]
  {::pc/output [{:demo-repos [:github.User/login :github.Repository/name]}]}
  {: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-incubator"}
    {:github.User/login "fulcrologic" :github.Repository/name "fulcro-spec"}
    {:github.User/login "thheller" :github.Repository/name "shadow-cljs"}]})

(def github-gql
  {::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.Repository/name}}
   ::p.http/driver p.http.fetch/request-async})

(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 #{">"}
                  ::p.http/driver          p.http.fetch/request-async}
     ::p/mutate  pc/mutate-async
     ::p/plugins [(pc/connect-plugin {::pc/register repositories
                                      ::pc/indexes  indexes})
                  p/error-handler-plugin
                  p/trace-plugin]}))

(defonce github-index-status
  (go-promise
    (<? (pcg/load-index github-gql indexes))))

(fm/defmutation github/addStar [_]
  (action [{:keys [state ref]}]
    (swap! state update-in ref assoc :github.Repository/viewerHasStarred true))
  (remote [_] true))

(fm/defmutation github/removeStar [_]
  (action [{:keys [state ref]}]
    (swap! state update-in ref assoc :github.Repository/viewerHasStarred false))
  (remote [_] true))

(fp/defsc Repository
  [this {:github.Repository/keys [id nameWithOwner viewerHasStarred]}]
  {:ident [:github.Repository/id :github.Repository/id]
   :query [:github.Repository/id :github.Repository/nameWithOwner :github.Repository/viewerHasStarred]}
  (dom/div
    (dom/div (str nameWithOwner))
    (if viewerHasStarred
      (dom/button {:onClick #(fp/transact! this [`{(github/removeStar {:github/input {:github/starrableId ~id}})
                                                   [:clientMutationId
                                                    {:starrable
                                                     [:viewerHasStarred]}]}])}
        "Remove star")
      (dom/button {:onClick #(fp/transact! this [`{(github/addStar {:github/input {:github/starrableId ~id}})
                                                   [:clientMutationId
                                                    {:starrable
                                                     [:viewerHasStarred]}]}])}
        "Add star"))))

(def repository (fp/factory Repository {:keyfn :github.Repository/id}))

(fp/defsc GraphqlDemo
  [this {:keys [demo-repos]}]
  {:initial-state (fn [_]
                    {})
   :ident         (fn [] [::root "singleton"])
   :query         [{:demo-repos (fp/get-query Repository)}]
   :css           []
   :css-include   []}
  (dom/div
    (mapv repository demo-repos)))

(def graphql-demo (fp/factory GraphqlDemo))

; setup the fulcro card to use in workspaces
(ws/defcard graphql-demo
  (ct.fulcro/fulcro-card
    {::f.portal/root GraphqlDemo
     ::f.portal/app  {:started-callback
                      (fn [app]
                        (go-catch
                          (try
                            (<? github-index-status)
                            (df/load app [::root "singleton"] GraphqlDemo)
                            (catch :default e (js/console.error "Error making index" e)))))

                      :networking
                      {:remote (-> parser
                                   (pfn/pathom-remote)
                                   (pfn/trace-remote))}}}))

; creates a parser view using pathom viz to explore the graph in workspaces
(ws/defcard graphql-demo-parser
  (pv.ws/pathom-card
    {::pv.ws/parser #(parser % %2)
     ::pv.ws/app    {:started-callback
                     (fn [app]
                       (go-catch
                         (try
                           (<? github-index-status)
                           ; after github schema is ready we request the editor to update
                           ; the index so the UI make it available right away
                           (pv.query-editor/load-indexes app)
                           (catch :default e (js/console.error "Error making index" e)))))}}))