diff --git a/.changeset/two-bulldogs-doubt.md b/.changeset/two-bulldogs-doubt.md new file mode 100644 index 00000000000..01a8939e915 --- /dev/null +++ b/.changeset/two-bulldogs-doubt.md @@ -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. diff --git a/packages/graphiql-toolkit/README.md b/packages/graphiql-toolkit/README.md index 15bd97f0dae..2848d8fba93 100644 --- a/packages/graphiql-toolkit/README.md +++ b/packages/graphiql-toolkit/README.md @@ -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 diff --git a/packages/graphiql-toolkit/docs/create-fetcher.md b/packages/graphiql-toolkit/docs/create-fetcher.md index 4e678526b5c..89120a07479 100644 --- a/packages/graphiql-toolkit/docs/create-fetcher.md +++ b/packages/graphiql-toolkit/docs/create-fetcher.md @@ -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 @@ -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'; @@ -123,6 +127,31 @@ export const App = () => ; ReactDOM.render(document.getElementByID('graphiql'), ); ``` +#### 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 = () => ; + +ReactDOM.render(document.getElementByID('graphiql'), ); +``` + #### Custom `fetcher` Example For SSR, we might want to use something like `isomorphic-fetch` @@ -148,4 +177,4 @@ ReactDOM.render(document.getElementByID('graphiql'), ); ## Credits -This is inspired from `graphql-subscriptions-fetcher` and thanks to @Urigo +This is originally inspired by `graphql-subscriptions-fetcher` created by @Urigo diff --git a/packages/graphiql-toolkit/package.json b/packages/graphiql-toolkit/package.json index 997856d3fc9..dbc8dba4b57 100644 --- a/packages/graphiql-toolkit/package.json +++ b/packages/graphiql-toolkit/package.json @@ -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" }, "keywords": [ "graphql", diff --git a/packages/graphiql-toolkit/src/__tests__/buildFetcher.spec.ts b/packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts similarity index 80% rename from packages/graphiql-toolkit/src/__tests__/buildFetcher.spec.ts rename to packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts index 2fd6de568bd..620177fa9fe 100644 --- a/packages/graphiql-toolkit/src/__tests__/buildFetcher.spec.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/__tests__/buildFetcher.spec.ts @@ -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 { @@ -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', () => { @@ -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([]); + }); }); diff --git a/packages/graphiql-toolkit/src/__tests__/lib.spec.ts b/packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts similarity index 51% rename from packages/graphiql-toolkit/src/__tests__/lib.spec.ts rename to packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts index 77367bb0dd1..48d18a7c99b 100644 --- a/packages/graphiql-toolkit/src/__tests__/lib.spec.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/__tests__/lib.spec.ts @@ -1,5 +1,9 @@ import { parse } from 'graphql'; -import { isSubscriptionWithName, createWebsocketsFetcherFromUrl } from '../lib'; +import { + isSubscriptionWithName, + createWebsocketsFetcherFromUrl, + getWsFetcher, +} from '../lib'; import 'isomorphic-fetch'; @@ -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); }); }); diff --git a/packages/graphiql-toolkit/src/createFetcher.ts b/packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts similarity index 81% rename from packages/graphiql-toolkit/src/createFetcher.ts rename to packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts index 910d9eb0ed0..55140749f30 100644 --- a/packages/graphiql-toolkit/src/createFetcher.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts @@ -4,8 +4,7 @@ import { createMultipartFetcher, createSimpleFetcher, isSubscriptionWithName, - createWebsocketsFetcherFromUrl, - createWebsocketsFetcherFromClient, + getWsFetcher, } from './lib'; /** @@ -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; } @@ -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; @@ -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.` }`, ); } diff --git a/packages/graphiql-toolkit/src/create-fetcher/index.ts b/packages/graphiql-toolkit/src/create-fetcher/index.ts new file mode 100644 index 00000000000..64e2d32f4fc --- /dev/null +++ b/packages/graphiql-toolkit/src/create-fetcher/index.ts @@ -0,0 +1,4 @@ +export * from './types'; +export { createGraphiQLFetcher } from './createFetcher'; + +// TODO: move the most useful utilities from graphiql to here diff --git a/packages/graphiql-toolkit/src/lib.ts b/packages/graphiql-toolkit/src/create-fetcher/lib.ts similarity index 84% rename from packages/graphiql-toolkit/src/lib.ts rename to packages/graphiql-toolkit/src/create-fetcher/lib.ts index 4616c9d4693..e37c79a16f7 100644 --- a/packages/graphiql-toolkit/src/lib.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/lib.ts @@ -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}`); } }; @@ -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); + } +}; diff --git a/packages/graphiql-toolkit/src/types.ts b/packages/graphiql-toolkit/src/create-fetcher/types.ts similarity index 93% rename from packages/graphiql-toolkit/src/types.ts rename to packages/graphiql-toolkit/src/create-fetcher/types.ts index fd188df535c..ea5c5731e56 100644 --- a/packages/graphiql-toolkit/src/types.ts +++ b/packages/graphiql-toolkit/src/create-fetcher/types.ts @@ -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. * diff --git a/packages/graphiql-toolkit/src/index.ts b/packages/graphiql-toolkit/src/index.ts index f424dd8eb62..a3c68903ec5 100644 --- a/packages/graphiql-toolkit/src/index.ts +++ b/packages/graphiql-toolkit/src/index.ts @@ -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 diff --git a/packages/graphiql/package.json b/packages/graphiql/package.json index 62ba33e9b5a..caf56655aa8 100644 --- a/packages/graphiql/package.json +++ b/packages/graphiql/package.json @@ -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", diff --git a/yarn.lock b/yarn.lock index 6cd85b5a170..340da584dd2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11137,17 +11137,10 @@ graphql-config@^3.0.2: string-env-interpolation "1.0.1" tslib "^2.0.0" -graphql-transport-ws@^1.9.0: - version "1.9.0" - resolved "https://registry.yarnpkg.com/graphql-transport-ws/-/graphql-transport-ws-1.9.0.tgz#81891de870619d4e39242954a9e0f832dd980179" - integrity sha512-yrw7nIR4V+lWWRCVCa5ogagHWjlPLDO/Ld1177V4S4fqcMO4qVJyTgMKbTcAUXBhlEATqN7Scb5Oy8Ly+zKFwg== - dependencies: - ws "^7.3.1" - -graphql-ws@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.1.0.tgz#cebe281474b5501d7be66210fb5711633b27fd78" - integrity sha512-DxJP1y2YzCqVLy7DrQN0iuR2l48vMOBWukX2d/J9aN2o5x9un5psIIq/2UFRh91UGARmfvPH86y1p4qbC1dITg== +graphql-ws@^4.3.2: + version "4.3.2" + resolved "https://registry.yarnpkg.com/graphql-ws/-/graphql-ws-4.3.2.tgz#c58b03acc3bd5d4a92a6e9f729d29ba5e90d46a3" + integrity sha512-jsW6eOlko7fJek1iaSGQFj97AWuhexL9A3PuxYtyke/VlMdbSFzmDR4PlPPCTBBskRg6tNRb5RTbBVSd2T60JQ== graphql@experimental-stream-defer: version "15.4.0-experimental-stream-defer.1" @@ -14471,10 +14464,10 @@ merge@^1.2.0: resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.1.tgz#38bebf80c3220a8a487b6fcfb3941bb11720c145" integrity sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ== -meros@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/meros/-/meros-1.1.2.tgz#12d5f520458ba8ae1536092824c1744fa09cf79d" - integrity sha512-BvOjEcEtGBOSts+lCCuqDe4LhSvzwQsQNxDB86ZY8RiAVQsPcmzxqm1/OjBBWv7vCufEEq8jstf4QJBBAHlDXg== +meros@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948" + integrity sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ== methods@~1.1.2: version "1.1.2" @@ -21333,11 +21326,6 @@ ws@^7.2.1, ws@^7.2.3: resolved "https://registry.yarnpkg.com/ws/-/ws-7.3.1.tgz#d0547bf67f7ce4f12a72dfe31262c68d7dc551c8" integrity sha512-D3RuNkynyHmEJIpD2qrgVkc9DQ23OrN/moAwZX4L8DfvszsJxpjQuUq3LMx6HoYji9fbIOBY18XWBsAux1ZZUA== -ws@^7.3.1: - version "7.4.2" - resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.2.tgz#782100048e54eb36fe9843363ab1c68672b261dd" - integrity sha512-T4tewALS3+qsrpGI/8dqNMLIVdq/g/85U98HPMa6F0m6xTbvhXU6RCQLqPH3+SlomNV/LdY6RXEbBpMH6EOJnA== - wsrun@^5.2.4: version "5.2.4" resolved "https://registry.yarnpkg.com/wsrun/-/wsrun-5.2.4.tgz#6eb6c3ccd3327721a8df073a5e3578fb0dea494e"