Skip to content

Commit c9dab6a

Browse files
committed
feat: new encapsulation for transport spec
1 parent 881d8b5 commit c9dab6a

File tree

31 files changed

+1278
-354
lines changed

31 files changed

+1278
-354
lines changed

examples/graphiql-create-react-app/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
"version": "0.1.11-alpha.8",
44
"private": true,
55
"dependencies": {
6-
"@types/react": "^16.9.34",
7-
"@types/react-dom": "^16.9.6",
86
"graphiql": "file:../../packages/graphiql",
97
"graphql": "^15.0.0",
108
"react": "^16.13.1",

examples/graphiql-parcel/package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,6 @@
2222
]
2323
},
2424
"dependencies": {
25-
"@types/react": "^16.9.34",
26-
"@types/react-dom": "^16.9.6",
2725
"graphiql": "file:../../packages/graphiql",
2826
"graphql": "15.0.0",
2927
"react": "^16.13.1",

package.json

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,21 @@
8181
"@strictsoftware/typedoc-plugin-monorepo": "^0.3.1",
8282
"@testing-library/jest-dom": "^5.4.0",
8383
"@types/codemirror": "^0.0.90",
84+
"@types/express": "^4.17.11",
8485
"@types/fetch-mock": "^7.3.2",
85-
"@types/jest": "^26.0.8",
86+
"@types/jest": "^26.0.20",
8687
"@types/node": "^13.11.1",
8788
"@types/prettier": "^2.0.0",
8889
"@types/theme-ui": "^0.3.1",
90+
"@types/ws": "^7.4.0",
8991
"@typescript-eslint/eslint-plugin": "^2.27.0",
9092
"@typescript-eslint/parser": "^2.27.0",
9193
"babel-eslint": "^10.1.0",
9294
"babel-jest": "^25.3.0",
9395
"conventional-changelog-conventionalcommits": "^4.2.3",
9496
"copy": "^0.3.2",
9597
"cross-env": "^7.0.2",
96-
"cypress": "^4.3.0",
98+
"cypress": "^4.7.0",
9799
"eslint": "^6.8.0",
98100
"eslint-config-prettier": "^6.10.1",
99101
"eslint-plugin-babel": "^5.3.0",
@@ -119,7 +121,7 @@
119121
"rimraf": "^3.0.2",
120122
"ts-jest": "^25.3.1",
121123
"typedoc": "^0.19.2",
122-
"typescript": "^3.9.5",
124+
"typescript": "^4.1.3",
123125
"whatwg-url": "^8.4.0"
124126
}
125127
}
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
## `@graphiql/build-fetcher`
2+
3+
a utility for generating a full-featured fetcher for GraphiQL.
4+
5+
under the hood, it uses [`graphql-ws`](https://www.npmjs.com/package/graphql-ws) and [`fetch-multipart-graphql`](https://www.npmjs.com/package/fetch-multipart-graphql) to follow the [GraphQL over HTTP Working Group Spec](https://github.com/graphql/graphql-over-http) both accepted and advanced proposals.
6+
7+
### Setup
8+
9+
[`graphiql`](https://npmjs.com/package/graphiql) and thus `react` and `react-dom` should already be installed.
10+
11+
you'll need to install `@graphiql/build-fetcher`
12+
13+
npm
14+
```bash
15+
npm install --save @graphiql/build-fetcher
16+
```
17+
18+
yarn
19+
```bash
20+
yarn add @graphiql/build-fetcher
21+
```
22+
23+
### Getting Started
24+
25+
We have a few flexible options to get you started with the client. It's meant to cover the majority of common use cases with a simple encapsulation.
26+
27+
#### HTTP/Multipart Usage
28+
29+
Here's a simple example. In this case, a websocket client isn't even initialized, only http (with multipart `@stream` and `@defer` support of course!).
30+
31+
```ts
32+
import * as React from "react"
33+
import ReactDOM from "react-dom"
34+
import { GraphiQL } from "graphiql"
35+
import { buildGraphiQLFetcher } from "@graphiql/build-fetcher"
36+
37+
const url = 'https://myschema.com/graphql'
38+
39+
const fetcher = buildGraphiQLFetcher({ url });
40+
41+
export const App = () => <GraphiQL fetcher={fetcher} />;
42+
43+
ReactDOM.render(document.getElementByID('graphiql'), <App />)
44+
```
45+
#### HTTP/Multipart & Websockets
46+
47+
Just by providing the `subscriptionsUrl`, you can generate a `graphql-ws` client
48+
49+
```ts
50+
import * as React from "react"
51+
import ReactDOM from "react-dom"
52+
import { GraphiQL } from "graphiql"
53+
import { buildGraphiQLFetcher } from "@graphiql/build-fetcher"
54+
55+
const url = 'https://myschema.com/graphql'
56+
57+
const subscriptionsUrl = "wss://myschema.com/graphql"
58+
59+
const fetcher = buildGraphiQLFetcher({
60+
url,
61+
subscriptionsUrl
62+
});
63+
64+
export const App = () => <GraphiQL fetcher={fetcher} />;
65+
66+
ReactDOM.render(document.getElementByID('graphiql'), <App />)
67+
```
68+
69+
You can further customize the `wsClient` implementation below
70+
71+
### Options
72+
73+
#### `url` (*required*)
74+
75+
This is url used for all `HTTP` requests, and for schema introspection.
76+
77+
#### `subscriptionsUrl`
78+
79+
This generates a `graphql-ws` client.
80+
81+
#### `wsClient`
82+
83+
provide your own subscriptions client. bypasses `subscriptionsUrl`. In theory, this could be any client using any transport, as long as it matches `graphql-ws` `Client` signature.
84+
85+
#### `headers`
86+
87+
Pass headers to any and all requests
88+
89+
#### `fetch`
90+
91+
Pass a custom fetch implementation such as `isomorphic-feth`
92+
93+
### Customization Examples
94+
95+
96+
97+
#### Custom `wsClient` Example
98+
Just by providing the `subscriptionsUrl`
99+
100+
```ts
101+
import * as React from "react"
102+
import ReactDOM from "react-dom"
103+
import { GraphiQL } from "graphiql"
104+
import { createClient } from "graphql-ws"
105+
import { buildGraphiQLFetcher } from "@graphiql/build-fetcher"
106+
107+
const url = 'https://myschema.com/graphql'
108+
109+
const subscriptionsUrl = "wss://myschema.com/graphql"
110+
111+
const fetcher = buildGraphiQLFetcher({
112+
url,
113+
wsClient: createClient({
114+
url: subscriptionsUrl ,
115+
keepAlive: 2000
116+
})
117+
});
118+
119+
export const App = () => <GraphiQL fetcher={fetcher} />;
120+
121+
ReactDOM.render(document.getElementByID('graphiql'), <App />)
122+
```
123+
124+
125+
#### Custom `fetcher` Example
126+
For SSR, we might want to use something like `isomorphic-fetch`
127+
128+
```ts
129+
import * as React from "react"
130+
import ReactDOM from "react-dom"
131+
import { GraphiQL } from "graphiql"
132+
import { fetch } from "isomorphic-fetch"
133+
import { buildGraphiQLFetcher } from "@graphiql/build-fetcher"
134+
135+
const url = 'https://myschema.com/graphql'
136+
137+
138+
const fetcher = buildGraphiQLFetcher({
139+
url,
140+
fetch
141+
});
142+
143+
export const App = () => <GraphiQL fetcher={fetcher} />;
144+
145+
ReactDOM.render(document.getElementByID('graphiql'), <App />)
146+
```
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const base = require('../../jest.config.base')(__dirname);
2+
3+
module.exports = {
4+
...base,
5+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
{
2+
"name": "@graphiql/build-fetcher",
3+
"version": "0.0.1",
4+
"description": "Utility to build a fetcher for GraphiQL",
5+
"contributors": [
6+
"Rikki Schulte <[email protected]> (http://rikki.dev/"
7+
],
8+
"repository": "http://github.com/graphql/graphiql",
9+
"homepage": "http://github.com/graphql/graphiql/tree/master/packages/graphiql#readme",
10+
"bugs": {
11+
"url": "https://github.com/graphql/graphiql/issues?q=issue+label:graphiql"
12+
},
13+
"license": "MIT",
14+
"main": "dist/index.js",
15+
"module": "esm/index.js",
16+
"scripts": {
17+
"server": "ts-node test/server.ts"
18+
},
19+
"dependencies": {
20+
"graphql-ws": "^4.1.0",
21+
"graphql-transport-ws": "^1.9.0",
22+
"fetch-multipart-graphql": "^3.0.0",
23+
"@n1ru4l/push-pull-async-iterable-iterator": "^2.0.1",
24+
"@graphiql/toolkit": "^0.0.1"
25+
},
26+
"devDependencies": {
27+
"isomorphic-fetch": "^3.0.0",
28+
"express": "^4.17.1",
29+
"express-graphql": "experimental-stream-defer",
30+
"ws": "^7.4.2",
31+
"ts-node": "^9.1.1"
32+
}
33+
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { parse, getIntrospectionQuery } from 'graphql';
2+
import { buildGraphiQLFetcher } from '../buildFetcher';
3+
4+
import 'isomorphic-fetch';
5+
6+
jest.mock('../lib');
7+
8+
jest.mock('graphql-ws');
9+
10+
jest.mock('graphql-transport-ws');
11+
12+
import {
13+
createWebsocketsClient,
14+
createWebsocketsFetcher,
15+
createMultipartFetcher,
16+
createSimpleFetcher,
17+
} from '../lib';
18+
import { createClient } from 'graphql-ws';
19+
20+
const exampleWithSubscripton = /* GraphQL */ `
21+
subscription Example {
22+
example
23+
}
24+
query SomethingElse {
25+
example
26+
}
27+
`;
28+
29+
const exampleWithSubscriptonNode = parse(exampleWithSubscripton);
30+
31+
const serverURL = 'http://localhost:3000/graphql';
32+
const wssURL = 'ws://localhost:3000/graphql';
33+
34+
const exampleIntrospectionJson = parse(getIntrospectionQuery());
35+
36+
describe('buildGraphiQLFetcher', () => {
37+
afterEach(() => {
38+
jest.resetAllMocks();
39+
});
40+
it('returns fetcher without websocket client by default', async () => {
41+
createWebsocketsClient.mockReturnValue(true);
42+
const fetcher = buildGraphiQLFetcher({ url: serverURL });
43+
expect(createWebsocketsClient.mock.calls).toEqual([]);
44+
expect(createMultipartFetcher.mock.calls).toEqual([
45+
[{ enableMultipart: true, url: serverURL }],
46+
]);
47+
48+
});
49+
it('returns fetcher without websocket client or multipart', () => {
50+
createWebsocketsClient.mockReturnValue(true);
51+
buildGraphiQLFetcher({ url: serverURL, enableMultipart: false });
52+
expect(createWebsocketsClient.mock.calls).toEqual([]);
53+
expect(createMultipartFetcher.mock.calls).toEqual([]);
54+
expect(createSimpleFetcher.mock.calls).toEqual([
55+
[{ enableMultipart: false, url: serverURL }, fetch],
56+
]);
57+
});
58+
it('returns fetcher with websocket client', () => {
59+
createWebsocketsClient.mockReturnValue('Client1');
60+
61+
const args = {
62+
url: serverURL,
63+
subscriptionsUrl: wssURL,
64+
enableMultipart: true,
65+
};
66+
67+
buildGraphiQLFetcher(args);
68+
69+
expect(createMultipartFetcher.mock.calls).toEqual([[args]]);
70+
expect(createWebsocketsClient.mock.calls).toEqual([[args]]);
71+
expect(createWebsocketsFetcher.mock.calls).toEqual([['Client1']]);
72+
});
73+
74+
it('returns fetcher with custom wsClient', () => {
75+
createClient.mockReturnValue('WSClient');
76+
createWebsocketsFetcher.mockReturnValue('CustomWSSFetcher');
77+
78+
const wsClient = createClient({ url: wssURL });
79+
const args = {
80+
url: serverURL,
81+
wsClient,
82+
enableMultipart: true,
83+
};
84+
85+
buildGraphiQLFetcher(args);
86+
87+
expect(createMultipartFetcher.mock.calls).toEqual([[args]]);
88+
expect(createWebsocketsClient.mock.calls).toEqual([]);
89+
expect(createWebsocketsFetcher.mock.calls).toEqual([['WSClient']]);
90+
});
91+
});
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { parse } from 'graphql';
2+
import { isSubcriptionWithName, createWebsocketsClient } from '../lib';
3+
4+
import "isomorphic-fetch"
5+
6+
7+
jest.mock('graphql-ws')
8+
9+
jest.mock("graphql-transport-ws")
10+
11+
12+
13+
import { createClient } from "graphql-ws"
14+
15+
import { createClient as createLegacyClient } from "graphql-transport-ws"
16+
17+
const exampleWithSubscripton = /* GraphQL */ parse(`
18+
subscription Example {
19+
example
20+
}
21+
query SomethingElse {
22+
example
23+
}
24+
`);
25+
26+
describe('isSubcriptionWithName', () => {
27+
it('detects when the subscription is present', () => {
28+
expect(
29+
isSubcriptionWithName(exampleWithSubscripton, 'Example'),
30+
).toBeTruthy();
31+
});
32+
it('detects when the specified operation is not a subscription', () => {
33+
expect(
34+
isSubcriptionWithName(exampleWithSubscripton, 'SomethingElse'),
35+
).toBeFalsy();
36+
});
37+
it('detects when the operation is not present', () => {
38+
expect(
39+
isSubcriptionWithName(exampleWithSubscripton, 'NotPresent'),
40+
).toBeFalsy();
41+
});
42+
});
43+
44+
describe('createWebsocketsClient', () => {
45+
afterEach(() => {
46+
// @ts-ignore
47+
createClient.mockRestore()
48+
})
49+
it('creates a websockets client using provided url', () => {
50+
createWebsocketsClient({
51+
url: 'https://example.com',
52+
subscriptionsUrl: 'wss://example.com',
53+
});
54+
// @ts-ignore
55+
expect(createClient.mock.calls[0][0]).toEqual({"url": "wss://example.com"})
56+
});
57+
});

0 commit comments

Comments
 (0)