Fulcro Integration
There are two main ways in which to use Pathom + GraphQL + Fulcro:
-
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.
-
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:
-
You can use any namespace on the query keywords.
-
The
name
portion of a keyword will be used to send to GraphQL
Mutations on a Simple GraphQL remote have the following rules:
-
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)))))}}))