Skip to content

Commit dd9397e

Browse files
authored
GraphiQL.createClient() accepts custom legacyClient, exports typescript types, fixes #1800. (#1819)
`createGraphiQLFetcher` now only attempts an `graphql-ws` connection when only `subscriptionUrl` is provided. In order to use `graphql-transport-ws`, you'll need to provide the `legacyClient` option only, and no `subscriptionUrl` or `wsClient` option.
1 parent 6869ce7 commit dd9397e

13 files changed

Lines changed: 171 additions & 94 deletions

File tree

.changeset/two-bulldogs-doubt.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
'@graphiql/toolkit': minor
3+
'graphiql': patch
4+
---
5+
6+
`GraphiQL.createClient()` accepts custom `legacyClient`, exports typescript types, fixes #1800.
7+
8+
`createGraphiQLFetcher` now only attempts an `graphql-ws` connection when only `subscriptionUrl` is provided. In order to use `graphql-transport-ws`, you'll need to provide the `legacyClient` option only, and no `subscriptionUrl` or `wsClient` option.

packages/graphiql-toolkit/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,16 @@
22

33
General purpose library as a dependency of GraphiQL.
44

5-
The goal is to make this and related packages a set of general purpose tools used to build an end implementation like GraphiQL
5+
Part of the GraphiQL 2.0.0 initiative.
66

7-
It also allows us to share utilities, libraries and components that can be used by
7+
## Docs
8+
9+
- **`createFetcher` [(Docs)](./docs/create-fetcher.md)** : a utility for creating a `fetcher` prop implementation for HTTP GET, POST including multipart, websockets fetcher
10+
- more to come!
811

912
## Todo
1013

1114
- [x] Begin porting common type definitions used by GraphiQL and it's dependencies
12-
- [ ] Port over the GraphiQL components library created by @walaura and designed by @orta
15+
- [ ] `createFetcher` utility for an easier `fetcher`
1316
- [ ] Migrate over general purpose `graphiql/src/utilities`
14-
- [ ] Frontend framework agnostic state implementation
15-
- [ ] React components and hooks? Or should react specifics live seperately?
17+
- [ ] Utility to generate json schema spec from `getQueryFacts` for monaco, vscode, etc

packages/graphiql-toolkit/docs/create-fetcher.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,12 +79,16 @@ This is url used for all `HTTP` requests, and for schema introspection.
7979

8080
#### `subscriptionUrl`
8181

82-
This generates a `graphql-ws` client.
82+
This generates a `graphql-ws` client using the provided url. Note that a server must be compatible with the new `graphql-ws` subscriptions spec for this to work.
8383

8484
#### `wsClient`
8585

8686
provide your own subscriptions client. bypasses `subscriptionUrl`. In theory, this could be any client using any transport, as long as it matches `graphql-ws` `Client` signature.
8787

88+
#### `legacyClient`
89+
90+
provide a legacy subscriptions client. bypasses `subscriptionUrl`. In theory, this could be any client using any transport, as long as it matches `subscriptions-transport-ws` `Client` signature.
91+
8892
#### `headers`
8993

9094
Pass headers to any and all requests
@@ -97,7 +101,7 @@ Pass a custom fetch implementation such as `isomorphic-feth`
97101

98102
#### Custom `wsClient` Example
99103

100-
Just by providing the `subscriptionUrl`
104+
Just by providing the `wsClient`
101105

102106
```ts
103107
import * as React from 'react';
@@ -123,6 +127,31 @@ export const App = () => <GraphiQL fetcher={fetcher} />;
123127
ReactDOM.render(document.getElementByID('graphiql'), <App />);
124128
```
125129

130+
#### Custom `legacyClient` Example
131+
132+
By providing the `legacyClient` you can support a `subscriptions-transport-ws` client implementation, or equivalent
133+
134+
```ts
135+
import * as React from 'react';
136+
import ReactDOM from 'react-dom';
137+
import { GraphiQL } from 'graphiql';
138+
import { SubscriptionClient } from 'subscriptions-transport-ws';
139+
import { createGraphiQLFetcher } from '@graphiql/toolkit';
140+
141+
const url = 'https://myschema.com/graphql';
142+
143+
const subscriptionUrl = 'wss://myschema.com/graphql';
144+
145+
const fetcher = createGraphiQLFetcher({
146+
url,
147+
legacyClient: new SubscriptionsClient(subscriptionUrl),
148+
});
149+
150+
export const App = () => <GraphiQL fetcher={fetcher} />;
151+
152+
ReactDOM.render(document.getElementByID('graphiql'), <App />);
153+
```
154+
126155
#### Custom `fetcher` Example
127156

128157
For SSR, we might want to use something like `isomorphic-fetch`
@@ -148,4 +177,4 @@ ReactDOM.render(document.getElementByID('graphiql'), <App />);
148177

149178
## Credits
150179

151-
This is inspired from `graphql-subscriptions-fetcher` and thanks to @Urigo
180+
This is originally inspired by `graphql-subscriptions-fetcher` created by @Urigo

packages/graphiql-toolkit/package.json

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,16 @@
2121
"scripts": {},
2222
"dependencies": {
2323
"@n1ru4l/push-pull-async-iterable-iterator": "^2.0.1",
24-
"graphql-ws": "^4.1.0",
25-
"meros": "^1.1.2",
26-
"subscriptions-transport-ws": "^0.9.18"
24+
"graphql-ws": "^4.3.2",
25+
"meros": "^1.1.4"
2726
},
2827
"devDependencies": {
2928
"isomorphic-fetch": "^3.0.0",
30-
"graphql": "experimental-stream-defer"
29+
"graphql": "experimental-stream-defer",
30+
"subscriptions-transport-ws": "^0.9.18"
31+
},
32+
"optionalDependencies": {
33+
"subscriptions-transport-ws": "^0.9.18"
3134
},
3235
"peerDependencies": {
3336
"graphql": ">= v14.5.0 <= 15.5.0"

packages/graphiql-toolkit/src/__tests__/buildFetcher.spec.ts renamed to packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,11 @@ import {
1313
createWebsocketsFetcherFromUrl,
1414
createMultipartFetcher,
1515
createSimpleFetcher,
16+
createWebsocketsFetcherFromClient,
17+
createLegacyWebsocketsFetcher,
1618
} from '../lib';
1719
import { createClient } from 'graphql-ws';
20+
import { SubscriptionClient } from 'subscriptions-transport-ws';
1821

1922
const exampleWithSubscripton = /* GraphQL */ `
2023
subscription Example {
@@ -85,9 +88,6 @@ describe('createGraphiQLFetcher', () => {
8588
createGraphiQLFetcher(args);
8689

8790
expect(createMultipartFetcher.mock.calls).toEqual([[args, fetch]]);
88-
expect(createWebsocketsFetcherFromUrl.mock.calls).toEqual([
89-
[args.subscriptionUrl],
90-
]);
9191
});
9292

9393
it('returns fetcher with custom wsClient', () => {
@@ -106,4 +106,23 @@ describe('createGraphiQLFetcher', () => {
106106
expect(createMultipartFetcher.mock.calls).toEqual([[args, fetch]]);
107107
expect(createWebsocketsFetcherFromUrl.mock.calls).toEqual([]);
108108
});
109+
110+
it('returns fetcher with custom legacyClient', () => {
111+
createClient.mockReturnValue('WSClient');
112+
createLegacyWebsocketsFetcher.mockReturnValue('CustomWSSFetcher');
113+
114+
const legacyClient = new SubscriptionClient(wssURL);
115+
const args = {
116+
url: serverURL,
117+
legacyClient,
118+
enableIncrementalDelivery: true,
119+
};
120+
121+
createGraphiQLFetcher(args);
122+
123+
expect(createMultipartFetcher.mock.calls).toEqual([[args, fetch]]);
124+
expect(createWebsocketsFetcherFromUrl.mock.calls).toEqual([]);
125+
expect(createWebsocketsFetcherFromClient.mock.calls).toEqual([]);
126+
expect(createLegacyWebsocketsFetcher.mock.calls).toEqual([]);
127+
});
109128
});

packages/graphiql-toolkit/src/__tests__/lib.spec.ts renamed to packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { parse } from 'graphql';
2-
import { isSubscriptionWithName, createWebsocketsFetcherFromUrl } from '../lib';
2+
import {
3+
isSubscriptionWithName,
4+
createWebsocketsFetcherFromUrl,
5+
getWsFetcher,
6+
} from '../lib';
37

48
import 'isomorphic-fetch';
59

@@ -48,14 +52,53 @@ describe('createWebsocketsFetcherFromUrl', () => {
4852
createWebsocketsFetcherFromUrl('wss://example.com');
4953
// @ts-ignore
5054
expect(createClient.mock.calls[0][0]).toEqual({ url: 'wss://example.com' });
51-
expect(SubscriptionClient.mock.calls).toEqual([]);
5255
});
5356

54-
it('creates a websockets client using provided url that fails to legacy client', async () => {
57+
it('creates a websockets client using provided url that fails', async () => {
5558
createClient.mockReturnValue(false);
56-
await createWebsocketsFetcherFromUrl('wss://example.com');
59+
expect(
60+
await createWebsocketsFetcherFromUrl('wss://example.com'),
61+
).toThrowError();
5762
// @ts-ignore
5863
expect(createClient.mock.calls[0][0]).toEqual({ url: 'wss://example.com' });
59-
expect(SubscriptionClient.mock.calls[0][0]).toEqual('wss://example.com');
64+
});
65+
});
66+
67+
describe('getWsFetcher', () => {
68+
afterEach(() => {
69+
jest.resetAllMocks();
70+
});
71+
it('provides an observable wsClient when custom wsClient option is provided', () => {
72+
createClient.mockReturnValue(true);
73+
getWsFetcher({
74+
url: '',
75+
// @ts-ignore
76+
wsClient: true,
77+
});
78+
// @ts-ignore
79+
expect(createClient.mock.calls).toHaveLength(0);
80+
});
81+
it('creates a subscriptions-transports-ws observable when custom legacyClient option is provided', () => {
82+
createClient.mockReturnValue(true);
83+
getWsFetcher({
84+
url: '',
85+
// @ts-ignore
86+
legacyClient: true,
87+
});
88+
// @ts-ignore
89+
expect(createClient.mock.calls).toHaveLength(0);
90+
expect(SubscriptionClient.mock.calls).toHaveLength(0);
91+
});
92+
93+
it('if subscriptionsUrl is provided, create a client on the fly', () => {
94+
createClient.mockReturnValue(true);
95+
getWsFetcher({
96+
url: '',
97+
subscriptionUrl: 'wss://example',
98+
});
99+
expect(createClient.mock.calls[0]).toEqual([
100+
{ connectionParams: undefined, url: 'wss://example' },
101+
]);
102+
expect(SubscriptionClient.mock.calls).toHaveLength(0);
60103
});
61104
});

packages/graphiql-toolkit/src/createFetcher.ts renamed to packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,7 @@ import {
44
createMultipartFetcher,
55
createSimpleFetcher,
66
isSubscriptionWithName,
7-
createWebsocketsFetcherFromUrl,
8-
createWebsocketsFetcherFromClient,
7+
getWsFetcher,
98
} from './lib';
109

1110
/**
@@ -18,7 +17,6 @@ import {
1817
*/
1918
export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
2019
let httpFetch;
21-
let wsFetcher: null | Fetcher | void = null;
2220
if (typeof window !== null && window?.fetch) {
2321
httpFetch = window.fetch;
2422
}
@@ -37,13 +35,7 @@ export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
3735
// simpler fetcher for schema requests
3836
const simpleFetcher = createSimpleFetcher(options, httpFetch);
3937

40-
if (options.subscriptionUrl) {
41-
wsFetcher = createWebsocketsFetcherFromUrl(options.subscriptionUrl);
42-
}
43-
if (options.wsClient) {
44-
wsFetcher = createWebsocketsFetcherFromClient(options.wsClient);
45-
}
46-
38+
const wsFetcher = getWsFetcher(options);
4739
const httpFetcher = options.enableIncrementalDelivery
4840
? createMultipartFetcher(options, httpFetch)
4941
: simpleFetcher;
@@ -65,7 +57,7 @@ export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
6557
`Your GraphiQL createFetcher is not properly configured for websocket subscriptions yet. ${
6658
options.subscriptionUrl
6759
? `Provided URL ${options.subscriptionUrl} failed`
68-
: `Try providing options.subscriptionUrl or options.wsClient first.`
60+
: `Please provide subscriptionUrl, wsClient or legacyClient option first.`
6961
}`,
7062
);
7163
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './types';
2+
export { createGraphiQLFetcher } from './createFetcher';
3+
4+
// TODO: move the most useful utilities from graphiql to here

packages/graphiql-toolkit/src/lib.ts renamed to packages/graphiql-toolkit/src/create-fetcher/lib.ts

Lines changed: 26 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -71,33 +71,16 @@ export const createWebsocketsFetcherFromUrl = (
7171
url: string,
7272
connectionParams?: ClientOptions['connectionParams'],
7373
) => {
74-
let wsClient: Client | null = null;
75-
let legacyClient: SubscriptionClient | null = null;
76-
if (url) {
77-
try {
78-
try {
79-
// TODO: defaults?
80-
wsClient = createClient({
81-
url,
82-
connectionParams,
83-
});
84-
if (!wsClient) {
85-
legacyClient = new SubscriptionClient(url, { connectionParams });
86-
}
87-
} catch (err) {
88-
legacyClient = new SubscriptionClient(url, { connectionParams });
89-
}
90-
} catch (err) {
91-
console.error(`Error creating websocket client for:\n${url}\n\n${err}`);
92-
}
93-
}
94-
95-
if (wsClient) {
74+
let wsClient;
75+
try {
76+
// TODO: defaults?
77+
wsClient = createClient({
78+
url,
79+
connectionParams,
80+
});
9681
return createWebsocketsFetcherFromClient(wsClient);
97-
} else if (legacyClient) {
98-
return createLegacyWebsocketsFetcher(legacyClient);
99-
} else if (url) {
100-
throw Error('subscriptions client failed to initialize');
82+
} catch (err) {
83+
console.error(`Error creating websocket client for:\n${url}\n\n${err}`);
10184
}
10285
};
10386

@@ -169,3 +152,20 @@ export const createMultipartFetcher = (
169152
yield chunk.map(part => part.body);
170153
}
171154
};
155+
156+
/**
157+
* If `wsClient` or `legacyClient` are provided, then `subscriptionUrl` is overridden.
158+
* @param options {CreateFetcherOptions}
159+
* @returns
160+
*/
161+
export const getWsFetcher = (options: CreateFetcherOptions) => {
162+
if (options.wsClient) {
163+
return createWebsocketsFetcherFromClient(options.wsClient);
164+
}
165+
if (options.legacyClient) {
166+
return createLegacyWebsocketsFetcher(options.legacyClient);
167+
}
168+
if (options.subscriptionUrl) {
169+
return createWebsocketsFetcherFromUrl(options.subscriptionUrl);
170+
}
171+
};

packages/graphiql-toolkit/src/types.ts renamed to packages/graphiql-toolkit/src/create-fetcher/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ export interface CreateFetcherOptions {
8989
* whether via `createClient()` itself or another client.
9090
*/
9191
wsClient?: Client;
92+
/**
93+
* `legacyClient` implementation that matches `subscriptions-transport-ws` signature,
94+
* whether via `new SubcriptionsClient()` itself or another client with a similar signature.
95+
*/
96+
legacyClient?: SubscriptionClient;
9297
/**
9398
* Headers you can provide statically.
9499
*

0 commit comments

Comments
 (0)