Skip to content

Commit

Permalink
GraphiQL.createClient() accepts custom legacyClient, exports type…
Browse files Browse the repository at this point in the history
…script 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.
  • Loading branch information
acao authored Apr 3, 2021
1 parent 6869ce7 commit dd9397e
Show file tree
Hide file tree
Showing 13 changed files with 171 additions and 94 deletions.
8 changes: 8 additions & 0 deletions .changeset/two-bulldogs-doubt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@graphiql/toolkit': minor
'graphiql': patch
---

`GraphiQL.createClient()` accepts custom `legacyClient`, exports typescript types, fixes #1800.

`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.
12 changes: 7 additions & 5 deletions packages/graphiql-toolkit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

General purpose library as a dependency of GraphiQL.

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

It also allows us to share utilities, libraries and components that can be used by
## Docs

- **`createFetcher` [(Docs)](./docs/create-fetcher.md)** : a utility for creating a `fetcher` prop implementation for HTTP GET, POST including multipart, websockets fetcher
- more to come!

## Todo

- [x] Begin porting common type definitions used by GraphiQL and it's dependencies
- [ ] Port over the GraphiQL components library created by @walaura and designed by @orta
- [ ] `createFetcher` utility for an easier `fetcher`
- [ ] Migrate over general purpose `graphiql/src/utilities`
- [ ] Frontend framework agnostic state implementation
- [ ] React components and hooks? Or should react specifics live seperately?
- [ ] Utility to generate json schema spec from `getQueryFacts` for monaco, vscode, etc
35 changes: 32 additions & 3 deletions packages/graphiql-toolkit/docs/create-fetcher.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,16 @@ This is url used for all `HTTP` requests, and for schema introspection.

#### `subscriptionUrl`

This generates a `graphql-ws` client.
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.

#### `wsClient`

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.

#### `legacyClient`

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.

#### `headers`

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

#### Custom `wsClient` Example

Just by providing the `subscriptionUrl`
Just by providing the `wsClient`

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

#### Custom `legacyClient` Example

By providing the `legacyClient` you can support a `subscriptions-transport-ws` client implementation, or equivalent

```ts
import * as React from 'react';
import ReactDOM from 'react-dom';
import { GraphiQL } from 'graphiql';
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { createGraphiQLFetcher } from '@graphiql/toolkit';

const url = 'https://myschema.com/graphql';

const subscriptionUrl = 'wss://myschema.com/graphql';

const fetcher = createGraphiQLFetcher({
url,
legacyClient: new SubscriptionsClient(subscriptionUrl),
});

export const App = () => <GraphiQL fetcher={fetcher} />;

ReactDOM.render(document.getElementByID('graphiql'), <App />);
```

#### Custom `fetcher` Example

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

## Credits

This is inspired from `graphql-subscriptions-fetcher` and thanks to @Urigo
This is originally inspired by `graphql-subscriptions-fetcher` created by @Urigo
11 changes: 7 additions & 4 deletions packages/graphiql-toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,16 @@
"scripts": {},
"dependencies": {
"@n1ru4l/push-pull-async-iterable-iterator": "^2.0.1",
"graphql-ws": "^4.1.0",
"meros": "^1.1.2",
"subscriptions-transport-ws": "^0.9.18"
"graphql-ws": "^4.3.2",
"meros": "^1.1.4"
},
"devDependencies": {
"isomorphic-fetch": "^3.0.0",
"graphql": "experimental-stream-defer"
"graphql": "experimental-stream-defer",
"subscriptions-transport-ws": "^0.9.18"
},
"optionalDependencies": {
"subscriptions-transport-ws": "^0.9.18"
},
"peerDependencies": {
"graphql": ">= v14.5.0 <= 15.5.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@ import {
createWebsocketsFetcherFromUrl,
createMultipartFetcher,
createSimpleFetcher,
createWebsocketsFetcherFromClient,
createLegacyWebsocketsFetcher,
} from '../lib';
import { createClient } from 'graphql-ws';
import { SubscriptionClient } from 'subscriptions-transport-ws';

const exampleWithSubscripton = /* GraphQL */ `
subscription Example {
Expand Down Expand Up @@ -85,9 +88,6 @@ describe('createGraphiQLFetcher', () => {
createGraphiQLFetcher(args);

expect(createMultipartFetcher.mock.calls).toEqual([[args, fetch]]);
expect(createWebsocketsFetcherFromUrl.mock.calls).toEqual([
[args.subscriptionUrl],
]);
});

it('returns fetcher with custom wsClient', () => {
Expand All @@ -106,4 +106,23 @@ describe('createGraphiQLFetcher', () => {
expect(createMultipartFetcher.mock.calls).toEqual([[args, fetch]]);
expect(createWebsocketsFetcherFromUrl.mock.calls).toEqual([]);
});

it('returns fetcher with custom legacyClient', () => {
createClient.mockReturnValue('WSClient');
createLegacyWebsocketsFetcher.mockReturnValue('CustomWSSFetcher');

const legacyClient = new SubscriptionClient(wssURL);
const args = {
url: serverURL,
legacyClient,
enableIncrementalDelivery: true,
};

createGraphiQLFetcher(args);

expect(createMultipartFetcher.mock.calls).toEqual([[args, fetch]]);
expect(createWebsocketsFetcherFromUrl.mock.calls).toEqual([]);
expect(createWebsocketsFetcherFromClient.mock.calls).toEqual([]);
expect(createLegacyWebsocketsFetcher.mock.calls).toEqual([]);
});
});
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { parse } from 'graphql';
import { isSubscriptionWithName, createWebsocketsFetcherFromUrl } from '../lib';
import {
isSubscriptionWithName,
createWebsocketsFetcherFromUrl,
getWsFetcher,
} from '../lib';

import 'isomorphic-fetch';

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

it('creates a websockets client using provided url that fails to legacy client', async () => {
it('creates a websockets client using provided url that fails', async () => {
createClient.mockReturnValue(false);
await createWebsocketsFetcherFromUrl('wss://example.com');
expect(
await createWebsocketsFetcherFromUrl('wss://example.com'),
).toThrowError();
// @ts-ignore
expect(createClient.mock.calls[0][0]).toEqual({ url: 'wss://example.com' });
expect(SubscriptionClient.mock.calls[0][0]).toEqual('wss://example.com');
});
});

describe('getWsFetcher', () => {
afterEach(() => {
jest.resetAllMocks();
});
it('provides an observable wsClient when custom wsClient option is provided', () => {
createClient.mockReturnValue(true);
getWsFetcher({
url: '',
// @ts-ignore
wsClient: true,
});
// @ts-ignore
expect(createClient.mock.calls).toHaveLength(0);
});
it('creates a subscriptions-transports-ws observable when custom legacyClient option is provided', () => {
createClient.mockReturnValue(true);
getWsFetcher({
url: '',
// @ts-ignore
legacyClient: true,
});
// @ts-ignore
expect(createClient.mock.calls).toHaveLength(0);
expect(SubscriptionClient.mock.calls).toHaveLength(0);
});

it('if subscriptionsUrl is provided, create a client on the fly', () => {
createClient.mockReturnValue(true);
getWsFetcher({
url: '',
subscriptionUrl: 'wss://example',
});
expect(createClient.mock.calls[0]).toEqual([
{ connectionParams: undefined, url: 'wss://example' },
]);
expect(SubscriptionClient.mock.calls).toHaveLength(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ import {
createMultipartFetcher,
createSimpleFetcher,
isSubscriptionWithName,
createWebsocketsFetcherFromUrl,
createWebsocketsFetcherFromClient,
getWsFetcher,
} from './lib';

/**
Expand All @@ -18,7 +17,6 @@ import {
*/
export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
let httpFetch;
let wsFetcher: null | Fetcher | void = null;
if (typeof window !== null && window?.fetch) {
httpFetch = window.fetch;
}
Expand All @@ -37,13 +35,7 @@ export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
// simpler fetcher for schema requests
const simpleFetcher = createSimpleFetcher(options, httpFetch);

if (options.subscriptionUrl) {
wsFetcher = createWebsocketsFetcherFromUrl(options.subscriptionUrl);
}
if (options.wsClient) {
wsFetcher = createWebsocketsFetcherFromClient(options.wsClient);
}

const wsFetcher = getWsFetcher(options);
const httpFetcher = options.enableIncrementalDelivery
? createMultipartFetcher(options, httpFetch)
: simpleFetcher;
Expand All @@ -65,7 +57,7 @@ export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
`Your GraphiQL createFetcher is not properly configured for websocket subscriptions yet. ${
options.subscriptionUrl
? `Provided URL ${options.subscriptionUrl} failed`
: `Try providing options.subscriptionUrl or options.wsClient first.`
: `Please provide subscriptionUrl, wsClient or legacyClient option first.`
}`,
);
}
Expand Down
4 changes: 4 additions & 0 deletions packages/graphiql-toolkit/src/create-fetcher/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './types';
export { createGraphiQLFetcher } from './createFetcher';

// TODO: move the most useful utilities from graphiql to here
Original file line number Diff line number Diff line change
Expand Up @@ -71,33 +71,16 @@ export const createWebsocketsFetcherFromUrl = (
url: string,
connectionParams?: ClientOptions['connectionParams'],
) => {
let wsClient: Client | null = null;
let legacyClient: SubscriptionClient | null = null;
if (url) {
try {
try {
// TODO: defaults?
wsClient = createClient({
url,
connectionParams,
});
if (!wsClient) {
legacyClient = new SubscriptionClient(url, { connectionParams });
}
} catch (err) {
legacyClient = new SubscriptionClient(url, { connectionParams });
}
} catch (err) {
console.error(`Error creating websocket client for:\n${url}\n\n${err}`);
}
}

if (wsClient) {
let wsClient;
try {
// TODO: defaults?
wsClient = createClient({
url,
connectionParams,
});
return createWebsocketsFetcherFromClient(wsClient);
} else if (legacyClient) {
return createLegacyWebsocketsFetcher(legacyClient);
} else if (url) {
throw Error('subscriptions client failed to initialize');
} catch (err) {
console.error(`Error creating websocket client for:\n${url}\n\n${err}`);
}
};

Expand Down Expand Up @@ -169,3 +152,20 @@ export const createMultipartFetcher = (
yield chunk.map(part => part.body);
}
};

/**
* If `wsClient` or `legacyClient` are provided, then `subscriptionUrl` is overridden.
* @param options {CreateFetcherOptions}
* @returns
*/
export const getWsFetcher = (options: CreateFetcherOptions) => {
if (options.wsClient) {
return createWebsocketsFetcherFromClient(options.wsClient);
}
if (options.legacyClient) {
return createLegacyWebsocketsFetcher(options.legacyClient);
}
if (options.subscriptionUrl) {
return createWebsocketsFetcherFromUrl(options.subscriptionUrl);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ export interface CreateFetcherOptions {
* whether via `createClient()` itself or another client.
*/
wsClient?: Client;
/**
* `legacyClient` implementation that matches `subscriptions-transport-ws` signature,
* whether via `new SubcriptionsClient()` itself or another client with a similar signature.
*/
legacyClient?: SubscriptionClient;
/**
* Headers you can provide statically.
*
Expand Down
16 changes: 1 addition & 15 deletions packages/graphiql-toolkit/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,2 @@
export * from './types';
export { createGraphiQLFetcher } from './createFetcher';

export type {
CreateFetcherOptions,
Fetcher,
FetcherOpts,
FetcherParams,
FetcherResult,
FetcherResultPayload,
FetcherReturnType,
Observable,
Unsubscribable,
} from './types';

export * from './create-fetcher';
// TODO: move the most useful utilities from graphiql to here
2 changes: 0 additions & 2 deletions packages/graphiql/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,6 @@
"express-graphql": "experimental-stream-defer",
"fork-ts-checker-webpack-plugin": "4.1.3",
"graphql": "experimental-stream-defer",
"graphql-transport-ws": "^1.9.0",
"graphql-ws": "^4.1.0",
"html-webpack-plugin": "^4.0.4",
"identity-obj-proxy": "^3.0.0",
"jest": "^24.8.0",
Expand Down
Loading

1 comment on commit dd9397e

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.