-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #4724 from rmosolgo/graphiql-subscription-fetchers
Add GraphiQL support for subscriptions
- Loading branch information
Showing
11 changed files
with
647 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
--- | ||
layout: guide | ||
doc_stub: false | ||
search: true | ||
section: JavaScript Client | ||
title: GraphiQL Subscriptions | ||
desc: Testing GraphQL subscriptions in the GraphiQL IDE | ||
index: 5 | ||
--- | ||
|
||
After setting up your server, you can integrate subscriptions into [GraphiQL](https://github.com/graphql/graphiql/tree/main/packages/graphiql#readme), the in-browser GraphQL IDE. | ||
|
||
## Adding GraphiQL to your app | ||
|
||
To get started, make a page for rendering GraphiQL, for example: | ||
|
||
```html | ||
<!-- views/graphiqls/show.html --> | ||
<div id="root" style="height: 100vh;"></div> | ||
``` | ||
|
||
Then, install GraphiQL (eg, `yarn add graphiql`) and add JavaScript code to import GraphiQL and render it on your page: | ||
|
||
```js | ||
import { GraphiQL } from 'graphiql'; | ||
import React from 'react'; | ||
import { createRoot } from 'react-dom/client'; | ||
import 'graphiql/graphiql.css'; | ||
import { createGraphiQLFetcher } from '@graphiql/toolkit'; | ||
|
||
const fetcher = createGraphiQLFetcher({ url: '/graphql' }); | ||
const root = createRoot(document.getElementById('root')); | ||
root.render(<GraphiQL fetcher={fetcher}/>); | ||
``` | ||
|
||
After that, you should be able to load the page in your app and see the GraphiQL editor. | ||
|
||
## Ably | ||
|
||
To integrate {% internal_link "Ably subscriptions", "subscriptions/ably_implementation" %}, use `createAblyFetcher`, for example: | ||
|
||
```js | ||
import Ably from "ably" | ||
import createAblyFetcher from 'graphql-ruby-client/subscriptions/createAblyFetcher'; | ||
|
||
// Initialize a client | ||
// the key must have "subscribe" and "presence" permissions | ||
const ably = new Ably.Realtime({ key: "your.application.key" }) | ||
|
||
// Initialize a new fetcher and pass it to GraphiQL below | ||
var fetcher = createAblyFetcher({ ably: ably, url: "/graphql" }) | ||
const root = createRoot(document.getElementById('root')); | ||
root.render(<GraphiQL fetcher={fetcher} />); | ||
``` | ||
|
||
Under the hood, it will use `window.fetch` to send GraphQL operations to the server, then listen for `X-Subscription-ID` headers in responses. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createAblyFetcher({ ... })`. | ||
|
||
## Pusher | ||
|
||
To integrate {% internal_link "Pusher subscriptions", "subscriptions/pusher_implementation" %}, use `createPusherFetcher`, for example: | ||
|
||
```js | ||
import Pusher from "pusher-js" | ||
import createPusherFetcher from 'graphql-ruby-client/subscriptions/createPusherFetcher'; | ||
|
||
// Initialize a client | ||
const pusher = new Pusher("your-app-key", { cluster: "your-cluster" }) | ||
|
||
// Initialize a new fetcher and pass it to GraphiQL below | ||
var fetcher = createPusherFetcher({ pusher: pusher, url: "/graphql" }) | ||
const root = createRoot(document.getElementById('root')); | ||
root.render(<GraphiQL fetcher={fetcher} />); | ||
``` | ||
|
||
Under the hood, it will use `window.fetch` to send GraphQL operations to the server, then listen for `X-Subscription-ID` headers in responses. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createPusherFetcher({ ... })`. | ||
|
||
## ActionCable | ||
|
||
To integrate {% internal_link "ActionCable subscriptions", "subscriptions/action_cable_implementation" %}, use `createActionCableFetcher`, for example: | ||
|
||
```js | ||
import { createConsumer } from "@rails/actioncable" | ||
import createActionCableFetcher from 'graphql-ruby-client/subscriptions/createActionCableFetcher'; | ||
|
||
// Initialize a client | ||
const actionCable = createConsumer() | ||
|
||
// Initialize a new fetcher and pass it to GraphiQL below | ||
var fetcher = createActionCableFetcher({ consumer: actionCable, url: "/graphql" }) | ||
const root = createRoot(document.getElementById('root')); | ||
root.render(<GraphiQL fetcher={fetcher} />); | ||
``` | ||
|
||
Under the hood, it will split traffic: it will send `subscription { ... }` operations via ActionCable and send queries and mutations via HTTP `POST` using `window.fetch`. To customize its HTTP requests, you can pass a `fetchOptions:` object or a custom `fetch:` function to `createPusherFetcher({ ... })`. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
105 changes: 105 additions & 0 deletions
105
javascript_client/src/subscriptions/__tests__/createAblyFetcherTest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
import createAblyFetcher from "../createAblyFetcher" | ||
import { Realtime } from "ably" | ||
|
||
function createAbly() { | ||
const _channels: {[key: string]: any } = {} | ||
|
||
const ably = { | ||
_channels: _channels, | ||
channels: { | ||
get(channelName: string) { | ||
return _channels[channelName] ||= { | ||
_listeners: [] as [string, Function][], | ||
name: channelName, | ||
presence: { | ||
enterClient(_clientName: string, _status: string) {}, | ||
leaveClient(_clientName: string) {}, | ||
}, | ||
detach(callback: Function) { | ||
callback() | ||
}, | ||
subscribe(eventName: string, callback: Function) { | ||
this._listeners.push([eventName, callback]) | ||
}, | ||
unsubscribe(){} | ||
} | ||
}, | ||
release(channelName: string) { | ||
delete _channels[channelName] | ||
} | ||
}, | ||
__testTrigger(channelName: string, eventName: string, data: any) { | ||
const channel = this.channels.get(channelName) | ||
const handler = channel._listeners.find((l: any) => l[0] == eventName) | ||
if (handler) { | ||
handler[1](data) | ||
} | ||
} | ||
} | ||
|
||
return ably | ||
} | ||
|
||
|
||
describe("createAblyFetcher", () => { | ||
it("yields updates for subscriptions", () => { | ||
const ably = createAbly() | ||
|
||
const fetchLog: any[] = [] | ||
const dummyFetch = function(url: string, fetchArgs: any) { | ||
fetchLog.push([url, fetchArgs.customOpt]) | ||
const dummyResponse = { | ||
json: () => { | ||
return { | ||
data: { | ||
hi: "First response" | ||
} | ||
} | ||
}, | ||
headers: { | ||
get() { | ||
return fetchArgs.body.includes("subscription") ? "abcd" : null | ||
} | ||
} | ||
} | ||
return Promise.resolve(dummyResponse) | ||
} | ||
|
||
const fetcher = createAblyFetcher({ | ||
ably: (ably as unknown) as Realtime, | ||
url: "/graphql", | ||
fetch: ((dummyFetch as unknown) as typeof fetch), | ||
fetchOptions: {customOpt: true} | ||
}) | ||
|
||
const result = fetcher({ | ||
variables: {}, | ||
operationName: "hello", | ||
body: "subscription hello { hi }" | ||
}, {}) | ||
|
||
return result.next().then((res) => { | ||
expect(res.value.data.hi).toEqual("First response") | ||
expect(fetchLog).toEqual([["/graphql", true]]) | ||
}).then(() => { | ||
const promise = result.next().then((res2) => { | ||
expect(res2).toEqual({ value: { data: { hi: "Bonjour" } }, done: false }) | ||
}) | ||
|
||
ably.__testTrigger("abcd", "update", { data: { result: { data: { hi: "Bonjour" } } } }) | ||
|
||
return promise.then(() => { | ||
// Test non-subscriptions too: | ||
expect(Object.keys(ably._channels)).toEqual(["abcd"]) | ||
const queryResult = fetcher({ variables: {}, operationName: null, body: "{ __typename }"}, {}) | ||
return queryResult.next().then((res) => { | ||
expect(res.value.data).toEqual({ hi: "First response"}) | ||
return queryResult.next().then((res2) => { | ||
expect(res2.done).toEqual(true) | ||
expect(ably._channels).toEqual({}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) | ||
}) |
66 changes: 66 additions & 0 deletions
66
javascript_client/src/subscriptions/__tests__/createActionCableFetcherTest.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import createActionCableFetcher from "../createActionCableFetcher" | ||
import type { Consumer } from "@rails/actioncable" | ||
import { parse } from "graphql" | ||
|
||
describe("createActionCableFetcherTest", () => { | ||
it("yields updates for subscriptions", () => { | ||
var handlers: any | ||
var log: [string, any][]= [] | ||
|
||
var dummyActionCableConsumer = { | ||
subscriptions: { | ||
create: (_conn: any, newHandlers: any) => { | ||
handlers = newHandlers | ||
return { | ||
perform: (evt: string, data: any) => { | ||
log.push([evt, data]) | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
const fetchLog: any[] = [] | ||
const dummyFetch = function(url: string, fetchArgs: any) { | ||
fetchLog.push([url, fetchArgs.custom]) | ||
return Promise.resolve({ json: () => { {} } }) | ||
} | ||
|
||
var options = { | ||
consumer: (dummyActionCableConsumer as unknown) as Consumer, | ||
url: "/graphql", | ||
fetch: dummyFetch as typeof fetch, | ||
fetchOptions: { | ||
custom: true, | ||
} | ||
} | ||
|
||
var fetcher = createActionCableFetcher(options) | ||
|
||
|
||
const queryStr = "subscription listen { update { message } }" | ||
const doc = parse(queryStr) | ||
|
||
const res = fetcher({ operationName: "listen", query: queryStr, variables: {}}, { documentAST: doc }) | ||
const promise = res.next().then((result) => { | ||
|
||
handlers.connected() // trigger the GraphQL send | ||
|
||
expect(result).toEqual({ value: { data: "hello" } , done: false }) | ||
expect(fetchLog).toEqual([]) | ||
expect(log).toEqual([ | ||
["execute", { operationName: "listen", query: queryStr, variables: {} }], | ||
]) | ||
}) | ||
|
||
handlers.received({ result: { data: "hello" } }) // simulate an update | ||
|
||
return promise.then(() => { | ||
let res2 = fetcher({ operationName: null, query: "{ __typename } ", variables: {}}, {}) | ||
const promise2 = res2.next().then(() => { | ||
expect(fetchLog).toEqual([["/graphql", true]]) | ||
}) | ||
return promise2 | ||
}) | ||
}) | ||
}) |
Oops, something went wrong.