Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[graphiql/toolkit] support graphql-sse #3750

Open
wants to merge 13 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/nasty-beers-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@graphiql/toolkit': minor
---

support graphql SSE for `options.subscriptionUrl`
7 changes: 6 additions & 1 deletion packages/graphiql-toolkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,22 @@
"devDependencies": {
"graphql": "^17.0.0-alpha.7",
"graphql-ws": "^5.5.5",
"graphql-sse": "^2.5.3",
"isomorphic-fetch": "^3.0.0",
"subscriptions-transport-ws": "0.11.0",
"tsup": "^8.2.4"
},
"peerDependencies": {
"graphql": "^15.5.0 || ^16.0.0 || ^17.0.0-alpha.2",
"graphql-ws": ">= 4.5.0"
"graphql-ws": ">= 4.5.0",
"graphql-sse": "^2"
},
"peerDependenciesMeta": {
"graphql-ws": {
"optional": true
},
"graphql-sse": {
"optional": true
}
},
"keywords": [
Expand Down
87 changes: 87 additions & 0 deletions packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type {
ClientOptions,
createClient as createClientType,
ExecutionResult,
} from 'graphql-sse';
import { Fetcher, FetcherParams } from './types';

/**
* Based on https://gist.github.com/enisdenjo/d7bc1a013433502349d2763c3d2f2b79
*/
export async function createSseFetcher(opts: ClientOptions): Promise<Fetcher> {

Check warning on line 11 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L11

Added line #L11 was not covered by tests
const { createClient } =
process.env.USE_IMPORT === 'false'
? (require('graphql-sse') as { createClient: typeof createClientType })
: await import('graphql-sse');

Check warning on line 15 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L14-L15

Added lines #L14 - L15 were not covered by tests

const sseClient = createClient({

Check warning on line 17 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L17

Added line #L17 was not covered by tests
retryAttempts: 0,
// @ts-expect-error
singleConnection: true, // or use false if you have an HTTP/2 server
Copy link

@derekwilling derekwilling Sep 10, 2024

Choose a reason for hiding this comment

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

How is this supposed to be configured by the user? Does it not support distinct connections mode?

I'm having issues getting the client to send POST requests (distinct connections mode) instead of PUT requests (single connection mode).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Nice catch, we need to pass options in

const sseFetcher = await createSseFetcher({
url: options.subscriptionUrl,
});

// @ts-expect-error
lazy: false, // connect as soon as the page opens
...opts,
});

function subscribe(payload: FetcherParams) {

Check warning on line 26 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L26

Added line #L26 was not covered by tests
let deferred: {
resolve: (arg: boolean) => void;
reject: (arg: unknown) => void;
};

const pending: ExecutionResult<Record<string, unknown>, unknown>[] = [];

Check warning on line 32 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L32

Added line #L32 was not covered by tests
let throwMe: unknown;
let done = false;

Check warning on line 34 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L34

Added line #L34 was not covered by tests

const dispose = sseClient.subscribe(

Check warning on line 36 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L36

Added line #L36 was not covered by tests
{
...payload,
// types are different with FetcherParams
operationName: payload.operationName ?? undefined,
},
{
next(data) {
pending.push(data);
deferred?.resolve(false);

Check warning on line 45 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L43-L45

Added lines #L43 - L45 were not covered by tests
},
error(err) {
throwMe = err;
deferred?.reject(throwMe);

Check warning on line 49 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L47-L49

Added lines #L47 - L49 were not covered by tests
},
complete() {
done = true;
deferred?.resolve(true);

Check warning on line 53 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L51-L53

Added lines #L51 - L53 were not covered by tests
},
},
);

return {
[Symbol.asyncIterator]() {
return this;

Check warning on line 60 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L58-L60

Added lines #L58 - L60 were not covered by tests
},
async next() {

Check warning on line 62 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L62

Added line #L62 was not covered by tests
if (done) {
return { done: true, value: undefined };

Check warning on line 64 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L64

Added line #L64 was not covered by tests
}
if (throwMe) {
throw throwMe;

Check warning on line 67 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L67

Added line #L67 was not covered by tests
}
if (pending.length) {
return { value: pending.shift() };

Check warning on line 70 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L70

Added line #L70 was not covered by tests
}
return (await new Promise((resolve, reject) => {
deferred = { resolve, reject };

Check warning on line 73 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L73

Added line #L73 was not covered by tests
}))
? { done: true, value: undefined }
: { value: pending.shift() };

Check warning on line 76 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L75-L76

Added lines #L75 - L76 were not covered by tests
},
async return() {
dispose();
return { done: true, value: undefined };

Check warning on line 80 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L78-L80

Added lines #L78 - L80 were not covered by tests
},
};
}

// @ts-expect-error todo: fix type
return subscribe;

Check warning on line 86 in packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/create-sse-fetcher.ts#L86

Added line #L86 was not covered by tests
}
16 changes: 14 additions & 2 deletions packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,12 @@
isSubscriptionWithName,
getWsFetcher,
} from './lib';
import { createSseFetcher } from './create-sse-fetcher';

/**
* build a GraphiQL fetcher that is:
* Build a GraphiQL fetcher that is:
* - backwards compatible
* - optionally supports graphql-ws or `
* - optionally supports graphql-ws or graphql-sse
*/
export function createGraphiQLFetcher(options: CreateFetcherOptions): Fetcher {
const httpFetch =
Expand Down Expand Up @@ -40,7 +41,18 @@
graphQLParams.operationName || undefined,
)
: false;

if (isSubscription) {
if (
options.subscriptionUrl &&
!options.subscriptionUrl.startsWith('ws')

Check warning on line 48 in packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts#L48

Added line #L48 was not covered by tests
) {
const sseFetcher = await createSseFetcher({

Check warning on line 50 in packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts#L50

Added line #L50 was not covered by tests
url: options.subscriptionUrl,
});
return sseFetcher(graphQLParams);

Check warning on line 53 in packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts

View check run for this annotation

Codecov / codecov/patch

packages/graphiql-toolkit/src/create-fetcher/createFetcher.ts#L53

Added line #L53 was not covered by tests
}

const wsFetcher = await getWsFetcher(options, fetcherOpts);

if (!wsFetcher) {
Expand Down
2 changes: 1 addition & 1 deletion packages/graphiql-toolkit/src/create-fetcher/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export interface CreateFetcherOptions {
*/
url: string;
/**
* url for websocket subscription requests
* url for websocket subscription requests or SSE
*/
subscriptionUrl?: string;
/**
Expand Down
2 changes: 1 addition & 1 deletion packages/graphiql-toolkit/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ const opts: Options = {
entry: ['src/**/*.ts', '!**/__tests__'],
bundle: false,
clean: true,
dts: true,
minifySyntax: true,
};

Expand All @@ -17,6 +16,7 @@ export default defineConfig([
env: {
USE_IMPORT: 'true',
},
dts: true,
},
{
...opts,
Expand Down
20 changes: 0 additions & 20 deletions packages/graphiql/cypress/e2e/graphql-ws.cy.ts

This file was deleted.

60 changes: 14 additions & 46 deletions packages/graphiql/cypress/e2e/incremental-delivery.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,36 +19,16 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => {
const mockStreamSuccess = {
data: {
streamable: [
{
text: 'Hi',
},
{
text: '你好',
},
{
text: 'Hola',
},
{
text: 'أهلاً',
},
{
text: 'Bonjour',
},
{
text: 'سلام',
},
{
text: '안녕',
},
{
text: 'Ciao',
},
{
text: 'हेलो',
},
{
text: 'Здорово',
},
{ text: 'Hi' },
{ text: '你好' },
{ text: 'Hola' },
{ text: 'أهلاً' },
{ text: 'Bonjour' },
{ text: 'سلام' },
{ text: '안녕' },
{ text: 'Ciao' },
{ text: 'हेलो' },
{ text: 'Здорово' },
],
},
};
Expand Down Expand Up @@ -141,22 +121,10 @@ describeOrSkip('IncrementalDelivery support via fetcher', () => {
person: {
name: 'Mark',
friends: [
{
name: 'James',
age: 1000,
},
{
name: 'Mary',
age: 1000,
},
{
name: 'John',
age: 1000,
},
{
name: 'Patrica',
age: 1000,
},
{ name: 'James', age: 1000 },
{ name: 'Mary', age: 1000 },
{ name: 'John', age: 1000 },
{ name: 'Patrica', age: 1000 },
],
age: 1000,
},
Expand Down
27 changes: 27 additions & 0 deletions packages/graphiql/cypress/e2e/ws-sse.cy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
describe('IncrementalDelivery support via fetcher', () => {
const testSubscription = /* GraphQL */ `
subscription Test {
message
}
`;

function assertResponse() {
for (const message of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
cy.assertQueryResult({ data: { message } });
}
}

it('should work with ws', () => {
cy.visit(`/?query=${testSubscription}`);
cy.clickExecuteQuery();
assertResponse();
});

it('should work with sse', () => {
cy.visit(
`/?subscriptionUrl=http://localhost:8080/graphql/stream&query=${testSubscription}`,
);
cy.clickExecuteQuery();
assertResponse();
});
});
3 changes: 2 additions & 1 deletion packages/graphiql/resources/renderExample.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ root.render(
React.createElement(GraphiQL, {
fetcher: GraphiQL.createFetcher({
url: getSchemaUrl(),
subscriptionUrl: 'ws://localhost:8081/subscriptions',
subscriptionUrl:
parameters.subscriptionUrl || 'ws://localhost:8081/subscriptions',
}),
query: parameters.query,
variables: parameters.variables,
Expand Down
29 changes: 29 additions & 0 deletions packages/graphiql/test/e2e-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ const {
const WebSocketsServer = require('./afterDevServer');
const schema = require('./schema');
const { customExecute } = require('./execute');
// eslint-disable-next-line import-x/no-extraneous-dependencies
const { createHandler } = require('graphql-sse/lib/use/express');

const app = express();

Expand All @@ -42,6 +44,33 @@ async function handler(req, res) {
sendResult(result, res);
}

app.use('/graphql/stream', (req, res, next) => {
// Fixes
// Access to fetch at 'http://localhost:8080/graphql/stream' from origin 'http://localhost:5173' has been blocked by
// CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin'
// header is present on the requested resource. If an opaque response serves your needs, set the request's mode to
// 'no-cors' to fetch the resource with CORS disabled.

// CORS headers
res.header('Access-Control-Allow-Origin', '*'); // restrict it to the required domain
res.header('Access-Control-Allow-Methods', 'GET,PUT,POST');
// Set custom headers for CORS
res.header(
'Access-Control-Allow-Headers',
'content-type,x-graphql-event-stream-token',
);

if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});

// Create the GraphQL over SSE handler
const sseHandler = createHandler({ schema, execute: customExecute });
// Serve all methods on `/graphql/stream`
app.use('/graphql/stream', sseHandler);

// Server
app.use(express.json());

Expand Down
2 changes: 1 addition & 1 deletion packages/graphiql/test/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,7 @@ const TestSubscriptionType = new GraphQLObjectType({
},
async *subscribe(root, args) {
for (const hi of ['Hi', 'Bonjour', 'Hola', 'Ciao', 'Zdravo']) {
if (args?.delay) {
if (args.delay) {
await sleep(args.delay);
}
yield { message: hi };
Expand Down
2 changes: 1 addition & 1 deletion packages/monaco-graphql/test/monaco-editor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ describe('monaco-editor', () => {
// expect(lines[1]).toMatch(' building for production...');
// expect(lines[2]).toBe('transforming...');
expect(lines[3]).toMatch(
`✓ ${parseInt(version, 10) > 16 ? 862 : 843} modules transformed.`,
`✓ ${parseInt(version, 10) > 16 ? 869 : 843} modules transformed.`,
);
// expect(lines[4]).toBe('rendering chunks...');
// expect(lines[5]).toBe('computing gzip size...');
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -10498,6 +10498,11 @@ graphql-http@^1.22.1:
resolved "https://registry.yarnpkg.com/graphql-http/-/graphql-http-1.22.1.tgz#3857ac75366e55db189cfe09ade9cc4c4f2cfd09"
integrity sha512-4Jor+LRbA7SfSaw7dfDUs2UBzvWg3cKrykfHRgKsOIvQaLuf+QOcG2t3Mx5N9GzSNJcuqMqJWz0ta5+BryEmXg==

graphql-sse@^2.5.3:
version "2.5.3"
resolved "https://registry.yarnpkg.com/graphql-sse/-/graphql-sse-2.5.3.tgz#c3557803f2db306d8ac87fd3bc089b6d4bac8353"
integrity sha512-5IcFW3e7fPOG7oFkK1v3X1wWtCJArQKB/H1UJeNotjy7a/9EYA5K+EiHJF1BiDSVNx7y64bd0FlDETrNBSvIHQ==

graphql-subscriptions@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/graphql-subscriptions/-/graphql-subscriptions-2.0.0.tgz#11ec181d475852d8aec879183e8e1eb94f2eb79a"
Expand Down
Loading