Skip to content

Commit

Permalink
New API and re-frame convention of single map arg (#90)
Browse files Browse the repository at this point in the history
  • Loading branch information
oliyh authored Jul 20, 2022
1 parent 2173f54 commit 2403e8d
Show file tree
Hide file tree
Showing 16 changed files with 2,366 additions and 705 deletions.
116 changes: 63 additions & 53 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

re-graph is a graphql client for Clojure and ClojureScript with bindings for [re-frame](https://github.com/Day8/re-frame) applications.

## Upgrade notice

:fire: Version `0.2.0` was recently released with breaking API changes. Please read the [Upgrade](UPGRADE.md) guide for more information.

## Notes

This library behaves like the popular [Apollo client](https://github.com/apollographql/subscriptions-transport-ws)
Expand Down Expand Up @@ -69,26 +73,26 @@ Call the `init` function to bootstrap it and then use `subscribe`, `unsubscribe`
;; initialise re-graph, possibly including configuration options (see below)
(re-graph/init {})

(defn on-thing [{:keys [data errors] :as payload}]
(defn on-thing [{:keys [data errors] :as response}]
;; do things with data
))

;; start a subscription, with responses sent to the callback-fn provided
(re-graph/subscribe :my-subscription-id ;; this id should uniquely identify this subscription
"{ things { id } }" ;; your graphql query
{:some "variable"} ;; arguments map
on-thing) ;; callback-fn when messages are recieved
(re-graph/subscribe {:id :my-subscription-id ;; this id should uniquely identify this subscription
:query "{ things { id } }" ;; your graphql query
:variables {:some "variable"} ;; arguments map
:callback on-thing}) ;; callback-fn when messages are recieved

;; stop the subscription
(re-graph/unsubscribe :my-subscription-id)
(re-graph/unsubscribe {:id :my-subscription-id})

;; perform a query, with the response sent to the callback event provided
(re-graph/query "{ things { id } }" ;; your graphql query
{:some "variable"} ;; arguments map
on-thing) ;; callback event when response is recieved
(re-graph/query {:query "{ things { id } }" ;; your graphql query
:variables {:some "variable"} ;; arguments map
:callback on-thing}) ;; callback event when response is recieved

;; shut re-graph down when finished
(re-graph/destroy)
(re-graph/destroy {})
```

### re-frame users
Expand All @@ -103,29 +107,31 @@ Dispatch the `init` event to bootstrap it and then use the `:subscribe`, `:unsub

(re-frame/reg-event-db
::on-thing
(fn [db [_ {:keys [data errors] :as payload}]]
;; do things with data e.g. write it into the re-frame database
))
[re-frame/unwrap]
(fn [db {:keys [response]}]
(let [{:keys [data errors]} response]
;; do things with data e.g. write it into the re-frame database
)))

;; start a subscription, with responses sent to the callback event provided
(re-frame/dispatch [::re-graph/subscribe
:my-subscription-id ;; this id should uniquely identify this subscription
"{ things { id } }" ;; your graphql query
{:some "variable"} ;; arguments map
[::on-thing]]) ;; callback event when messages are recieved
{:id :my-subscription-id ;; this id should uniquely identify this subscription
:query "{ things { id } }" ;; your graphql query
:variables {:some "variable"} ;; arguments map
:callback [::on-thing]}]) ;; callback event when messages are recieved

;; stop the subscription
(re-frame/dispatch [::re-graph/unsubscribe :my-subscription-id])
(re-frame/dispatch [::re-graph/unsubscribe {:id :my-subscription-id}])

;; perform a query, with the response sent to the callback event provided
(re-frame/dispatch [::re-graph/query
:my-query-id ;; unique id for this query
"{ things { id } }" ;; your graphql query
{:some "variable"} ;; arguments map
[::on-thing]]) ;; callback event when response is recieved
{:id :my-query-id ;; unique id for this query
:query "{ things { id } }" ;; your graphql query
:variables {:some "variable"} ;; arguments map
:callback [::on-thing]}]) ;; callback event when response is recieved

;; shut re-graph down when finished
(re-frame/dispatch [::re-graph/destroy])
(re-frame/dispatch [::re-graph/destroy {}])
```

### Options
Expand All @@ -135,23 +141,23 @@ Options can be passed to the init event, with the following possibilities:
```clojure
(re-frame/dispatch
[::re-graph/init
{:ws {:url "wss://foo.io/graphql-ws" ;; override the websocket url (defaults to /graphql-ws, nil to disable)
:sub-protocol "graphql-ws" ;; override the websocket sub-protocol (defaults to "graphql-ws")
:reconnect-timeout 5000 ;; attempt reconnect n milliseconds after disconnect (defaults to 5000, nil to disable)
:resume-subscriptions? true ;; start existing subscriptions again when websocket is reconnected after a disconnect (defaults to true)
:connection-init-payload {} ;; the payload to send in the connection_init message, sent when a websocket connection is made (defaults to {})
:impl {} ;; implementation-specific options (see hato for options, defaults to {}, may be a literal or a function that returns the options)
:supported-operations #{:subscribe ;; declare the operations supported via websocket, defaults to all three
:query ;; if queries/mutations must be done via http set this to #{:subscribe} only
:mutate}
}

:http {:url "http://bar.io/graphql" ;; override the http url (defaults to /graphql)
:impl {} ;; implementation-specific options (see clj-http or hato for options, defaults to {}, may be a literal or a function that returns the options)
:supported-operations #{:query ;; declare the operations supported via http, defaults to :query and :mutate
{:ws {:url "wss://foo.io/graphql-ws" ;; override the websocket url (defaults to /graphql-ws, nil to disable)
:sub-protocol "graphql-ws" ;; override the websocket sub-protocol (defaults to "graphql-ws")
:reconnect-timeout 5000 ;; attempt reconnect n milliseconds after disconnect (defaults to 5000, nil to disable)
:resume-subscriptions? true ;; start existing subscriptions again when websocket is reconnected after a disconnect (defaults to true)
:connection-init-payload {} ;; the payload to send in the connection_init message, sent when a websocket connection is made (defaults to {})
:impl {} ;; implementation-specific options (see hato for options, defaults to {}, may be a literal or a function that returns the options)
:supported-operations #{:subscribe ;; declare the operations supported via websocket, defaults to all three
:query ;; if queries/mutations must be done via http set this to #{:subscribe} only
:mutate}
}
}])
}

:http {:url "http://bar.io/graphql" ;; override the http url (defaults to /graphql)
:impl {} ;; implementation-specific options (see clj-http or hato for options, defaults to {}, may be a literal or a function that returns the options)
:supported-operations #{:query ;; declare the operations supported via http, defaults to :query and :mutate
:mutate}
}
}])
```

Either `:ws` or `:http` can be set to nil to disable the WebSocket or HTTP protocols.
Expand All @@ -165,34 +171,38 @@ All function/event signatures now take an optional instance-name as the first ar
(require '[re-graph.core :as re-graph])

;; initialise re-graph for service A
(re-graph/init :service-a {:ws-url "wss://a.com/graphql-ws})
(re-graph/init {:instance-id :service-a
:ws {:url "wss://a.com/graphql-ws}})
;; initialise re-graph for service B
(re-graph/init :service-b {:ws-url "wss://b.net/api/graphql-ws})
(re-graph/init {:instance-id :service-b
:ws {:url "wss://b.com/api/graphql-ws}})

(defn on-a-thing [{:keys [data errors] :as payload}]
;; do things with data from service A
))

;; subscribe to service A, events will be sent to the on-a-thing callback
(re-graph/subscribe :service-a ;; the instance-name you want to talk to
:my-subscription-id ;; this id should uniquely identify this subscription for this service
"{ things { a } }"
on-a-thing)
(re-graph/subscribe {:instance-id :service-a ;; the instance-name you want to talk to
:id :my-subscription-id ;; this id should uniquely identify this subscription for this service
:query "{ things { a } }"
:callback on-a-thing})

(defn on-b-thing [{:keys [data errors] :as payload}]
;; do things with data from service B
))

;; subscribe to service B, events will be sent to the on-b-thing callback
(re-graph/subscribe :service-b ;; the instance-name you want to talk to
:my-subscription-id
"{ things { b } }"
on-b-thing)
(re-graph/subscribe {:instance-id :service-b ;; the instance-name you want to talk to
:id :my-subscription-id
:query "{ things { a } }"
:callback on-b-thing})

;; stop the subscriptions
(re-graph/unsubscribe :service-a :my-subscription-id)
(re-graph/unsubscribe :service-b :my-subscription-id)
(re-graph/unsubscribe {:instance-id :service-a
:id :my-subscription-id})
(re-graph/unsubscribe {:instance-id :service-b
:id :my-subscription-id})
```

## Authentication
Expand Down Expand Up @@ -241,7 +251,7 @@ When using re-graph within a browser, site cookies are shared between HTTP and W

When using re-graph with Clojure, however, some configuration is necessary to ensure that the same cookie store is used for both HTTP and WebSocket connections.

Before initializing re-graph, create a common HTTP client.
Before initialising re-graph, create a common HTTP client.

```
(ns user
Expand All @@ -254,7 +264,7 @@ Before initializing re-graph, create a common HTTP client.

See the [hato documentation](https://github.com/gnarroway/hato) for all the supported configuration options.

When initializing re-graph, configure both the HTTP and WebSocket connections with this client:
When initialising re-graph, configure both the HTTP and WebSocket connections with this client:

```
(re-graph/init {:http {:impl {:http-client http-client}}
Expand Down
187 changes: 187 additions & 0 deletions UPGRADE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
# Upgrading from 0.1.x to 0.2.0

- [Rationale](#rationale)
- [Spec and Instrumentation](#spec-and-instrumentation)
- [API changes](#api-changes)
- [Legacy API](#legacy-api)

## Rationale

The signature of re-graph events had become overloaded with two features:
- Multi instances, where an `instance-name` could be optionally supplied as the first argument to address a particular instance of re-graph
- Query deduplication and cancellation, where a `query-id` could be optionally supplied as the first argument for queries and mutations

In the case where you were supplying both `instance-name` and `query-id` then these would be given as the first two arguments respectively.

This made destructuring re-graph events difficult and error prone, resulting in hard-to-read error messages when something went wrong.

At the same time, [re-frame](https://github.com/day8/re-frame/issues/644) was discussing the same issue on a more generic level,
namely that positional significance of arguments becomes problematic with large numbers of arguments. To quote their rationale:

> For software systems, the arrow of time and the arrow of entropy are aligned. Changes inevitably happen.
>
> When we use vectors in both these situations we are being "artificially placeful". The elements of the vector have names, but we are pretending they have indexes. That little white lie has some benefits (is terse and minimal) but it also makes the use of these structures a bit fragile WRT to change. Names (truth!) are more flexible and robust than indexes (a white lie).
>
> The benefit of using names accumulate on two time scales:
>
> - within a single execution of the app (as data moves around on the inside of your app)
> - across the maintenance cycle of the app's codebase (as your app's code changes in response to user feedback)
re-frame has added an `unwrap` interceptor to make working with a single map argument easier, and re-graph now follows
this convention with consistently-named keys to remove ambiguity.

## Spec and Instrumentation

The re-graph API is now described by [re-graph.spec](https://github.com/oliyh/re-graph/blob/re-frame-maps/src/re_graph/spec.cljc).
All functions have specs and the implementation re-frame events also have an assertion on the event payload.

This means you can validate your calls to re-graph by turning on instrumentation (for the Clojure/script API) and/or assertions (for re-frame users):

```clj
;; instrumentation
(require '[clojure.spec.test.alpha :as stest])
(stest/instrument)

;; assertions
(require '[clojure.spec.alpha :as s])
(s/check-asserts true)
```

## API changes

All re-frame events (and all vanilla functions) in re-graph's `core` api now accept a single map argument.

Queries, mutations and subscriptions are closely aligned, accepting `:query`, `:variables`, `:callback` and `:id`.

Note that it is expected re-frame callbacks also follow the same convention of a single map argument, with `:response` containing the response from the server.
Vanilla Clojure/script API callback functions remain unchanged.

Examples below show mostly the re-frame events but the Clojure/script API functions take the exact same map arguments.

### Examples

#### re-frame callbacks

A re-frame callback that was originally defined and invoked as follows:
```clj
(rf/reg-event-db
::my-callback
(fn [db [_ response]]
(assoc db :response response)))

(rf/dispatch [::re-graph/query "{ some { thing } }" {:some "variable"} [::my-callback]])
```
Should now expect the response to be under the `:response` key in a single map argument (destructured here using `unwrap`):
```clj
(rf/reg-event-db
::my-callback
[rf/unwrap]
(fn [db {:keys [response]}]
(assoc db :response response)))

(rf/dispatch [::re-graph/query {:query "{ some { thing } }"
:variables {:some "variable"}
:callback [::my-callback]}])
```

Any partial params supplied like `my-opts` shown here:
```clj
(rf/reg-event-db
::my-callback
(fn [db [_ my-opts response]]
(assoc db :response response)))

(rf/dispatch [::re-graph/query "{ some { thing } }" {:some "variable"} [::my-callback {:my-opts true}]])
```
Should now be used like this:
```clj
(rf/reg-event-db
::my-callback
[rf/unwrap]
(fn [db {:keys [my-opts response]}]
(assoc db :response response)))

(rf/dispatch [::re-graph/query {:query "{ some { thing } }"
:variables {:some "variable"}
:callback [::my-callback {:my-opts true}]}])
```

#### Simple query

A query with some variables and a callback:
```clj
(rf/dispatch [::re-graph/query "{ some { thing } }" {:some "variable"} [::my-callback]])
```
becomes
```clj
(rf/dispatch [::re-graph/query {:query "{ some { thing } }"
:variables {:some "variable"}
:callback [::my-callback]}])
```

Or, if you were using the vanilla Clojure/script API:
```clj
(re-graph/query "{ some { thing } }" {:some "variable"} (fn [response] ...))
```
becomes
```clj
(re-graph/query {:query "{ some { thing } }"
:variables {:some "variable"}
:callback (fn [response] ...)})
```


#### Subscriptions

Subscriptions have always required an id, which could optionally be used for queries and mutations as well. These are now named `:id`:
```clj
(rf/dispatch [::re-graph/subscribe :my-subscription-id "{ some { thing } }" {:some "variable"} [::my-callback]])
```
becomes
```clj
(rf/dispatch [::re-graph/subscribe {:id :my-subscription-id
:query "{ some { thing } }"
:variables {:some "variable"}
:callback [::my-callback]}])
```

And to unsubscribe:
```clj
(rf/dispatch [::re-graph/unsubscribe :my-subscription-id])
```
becomes
```clj
(rf/dispatch [::re-graph/unsubscribe {:id :my-subscription-id}])
```

#### Multiple instances

You can supply `:instance-id` to the `init` (and `re-init`) events and to any subsequent queries:
```clj
(rf/dispatch [::re-graph/init :my-service {:ws {:url "https://my.service"}}])

(rf/dispatch [::re-graph/query :my-service "{ some { thing } }" {:some "variable"} [::my-callback]])
```
becomes
```clj
(rf/dispatch [::re-graph/init {:instance-id :my-service
:ws {:url "https://my.service"}}])

(rf/dispatch [::re-graph/query {:instance-id :my-service
:query "{ some { thing } }"
:variables {:some "variable"}
:callback [::my-callback]}])
```

And to destroy:
```clj
(rf/dispatch [::re-graph/destroy :my-service])
```
becomes
```clj
(rf/dispatch [::re-graph/destroy {:instance-id :my-service}])
```

## Legacy API

The original API is available at `re-graph.core-deprecated`. This will be removed in a future release.
2 changes: 1 addition & 1 deletion clj-http-gniazdo/project.clj
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
(defproject re-graph.clj-http-gniazdo "0.1.18-SNAPSHOT"
(defproject re-graph.clj-http-gniazdo "0.2.0-SNAPSHOT"
:dependencies [[clj-http "3.12.3"]
[stylefruits/gniazdo "1.2.0"]])
Loading

0 comments on commit 2403e8d

Please sign in to comment.