Skip to content

Commit 66c8d2c

Browse files
authored
feat: helix defer, stream and subscription example (#187)
1 parent 114945f commit 66c8d2c

File tree

6 files changed

+802
-1
lines changed

6 files changed

+802
-1
lines changed
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
## Envelop example of GraphQL-Helix with `@defer` and `@stream`
2+
3+
This example demonstrate how to implement the basic GraphQL Envelop flow with Envelop and [`graphql-helix`](https://github.com/contrawork/graphql-helix).
4+
5+
GraphQL-Helix provides a GraphQL execution flow, that abstract the HTTP execution, and allows you to easily support multiple transports, based on your needs.
6+
7+
Additionally, it has built in support for the [`@defer` and `@stream` directives](https://github.com/graphql/graphql-spec/blob/main/rfcs/DeferStream.md), allowing [incremental delivery over HTTP](https://github.com/graphql/graphql-over-http/blob/main/rfcs/IncrementalDelivery.md).
8+
9+
Subscriptions over [Server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) are also supported, which simplify overhead required for setting up subscriptions over a more complex protocol like graphql-transport-ws.
10+
11+
This examples uses the experimental `graphql.js` version `15.4.0-experimental-stream-defer.1` for showcasing stream and defer usage.
12+
13+
## Running this example
14+
15+
1. `cd` into ththisat folder
16+
1. Install all dependencies (using `yarn`)
17+
1. Run `yarn start`.
18+
1. Open http://localhost:3000/graphql in your browser, and try one of the suggested operations.
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/* eslint-disable no-console */
2+
import fastify from 'fastify';
3+
import { getGraphQLParameters, processRequest, renderGraphiQL, shouldRenderGraphiQL } from 'graphql-helix';
4+
import { envelop, useLogger, useSchema, useTiming } from '@envelop/core';
5+
import { makeExecutableSchema } from '@graphql-tools/schema';
6+
7+
const sleep = (t = 1000) => new Promise(resolve => setTimeout(resolve, t));
8+
9+
const schema = makeExecutableSchema({
10+
typeDefs: /* GraphQL */ `
11+
type Query {
12+
hello: String!
13+
greetings: [String!]
14+
me: User!
15+
}
16+
17+
type User {
18+
id: ID!
19+
name: String!
20+
friends: [User!]!
21+
bio: String!
22+
}
23+
24+
type Subscription {
25+
clock: String
26+
}
27+
`,
28+
resolvers: {
29+
Query: {
30+
hello: () => 'World',
31+
greetings: async function* () {
32+
for (const greeting of ['hi', 'ho', 'sup', 'ola', 'bonjour']) {
33+
yield greeting;
34+
await sleep();
35+
}
36+
},
37+
me: () => ({ id: '1', name: 'Vanja' }),
38+
},
39+
User: {
40+
bio: async () => {
41+
await sleep(1500);
42+
return 'I like turtles';
43+
},
44+
friends: async function* () {
45+
for (const user of [
46+
{ id: '2', name: 'Angela' },
47+
{ id: '3', name: 'Christopher' },
48+
{ id: '4', name: 'Titiana' },
49+
{ id: '5', name: 'Leonard' },
50+
{ id: '6', name: 'Ernesto' },
51+
]) {
52+
yield user;
53+
await sleep(1000);
54+
}
55+
},
56+
},
57+
Subscription: {
58+
clock: {
59+
subscribe: async function* () {
60+
while (true) {
61+
yield { clock: new Date().toString() };
62+
await sleep();
63+
}
64+
},
65+
},
66+
},
67+
},
68+
});
69+
70+
const graphiQLContent = /* GraphQL */ `
71+
##
72+
## Welcome to the envelop graphql-helix demo.
73+
##
74+
## We prepared some operations for you to try out :)
75+
##
76+
77+
# Basic query, nothing fancy here :)
78+
query BasicQuery {
79+
hello
80+
}
81+
82+
# Query using stream
83+
#
84+
# stream can be used on fields that return lists.
85+
# The resolver on the backend uses an async generator function for yielding the values.
86+
# In this demo there is a sleep of one second before the next value is yielded.
87+
#
88+
# stream is useful in scenarios where a lot of items must be sent to the client, but you want to show something as soon as possible
89+
# e.g. a social media feed.
90+
#
91+
# The initialCount argument specifies the amount of items sent within the initial chunk.
92+
query StreamQuery {
93+
greetings @stream(initialCount: 1)
94+
}
95+
96+
# Query using defer
97+
#
98+
# defer can be used on fragments in order to defer sending the result to the client, if it takes longer than the rest of the resolvers to yield an value.
99+
# The User.bio resolver on the backend uses a sleep of 2 seconds for deferring the resolution of the value.
100+
# Stream is useful when a certain resolver on your backend is slow, but not mandatory for showing something meaningful to your users.
101+
# An example for this would be a slow database call or third-party service.
102+
query DeferQuery {
103+
me {
104+
id
105+
name
106+
... on User @defer {
107+
bio
108+
}
109+
}
110+
}
111+
112+
# Query using both stream and defer
113+
#
114+
# Both directives can be used on the same operation!
115+
query MixedStreamAndDefer {
116+
me {
117+
id
118+
name
119+
... on User @defer {
120+
bio
121+
}
122+
friends @stream(initialCount: 1) {
123+
id
124+
name
125+
}
126+
}
127+
}
128+
129+
# Basic Subscription
130+
#
131+
# A subscription is a persistent connection between the graphql client and server and can be used for pushing events to the client.
132+
#
133+
# This subscription publishes the current date string every second.
134+
# Subscriptions are similar to defer and stream implemented via async generators.
135+
# Any event source such as Redis PubSub or MQTT can be wrapped in an async generator and used for backing the subscription.
136+
# The published event value is then passed on to the execution algorithm similar to mutations and subscriptions.
137+
subscription BasicSubscription {
138+
clock
139+
}
140+
`;
141+
142+
const getEnveloped = envelop({
143+
plugins: [useSchema(schema), useLogger(), useTiming()],
144+
});
145+
const app = fastify();
146+
147+
app.route({
148+
method: ['GET', 'POST'],
149+
url: '/graphql',
150+
async handler(req, res) {
151+
const { parse, validate, contextFactory, execute, schema } = getEnveloped();
152+
const request = {
153+
body: req.body,
154+
headers: req.headers,
155+
method: req.method,
156+
query: req.query,
157+
};
158+
159+
if (shouldRenderGraphiQL(request)) {
160+
res.type('text/html');
161+
res.send(
162+
renderGraphiQL({
163+
defaultQuery: graphiQLContent
164+
.split('\n')
165+
.slice(1)
166+
.map(line => line.replace(' ', ''))
167+
.join('\n'),
168+
})
169+
);
170+
} else {
171+
const request = {
172+
body: req.body,
173+
headers: req.headers,
174+
method: req.method,
175+
query: req.query,
176+
};
177+
const { operationName, query, variables } = getGraphQLParameters(request);
178+
const result = await processRequest({
179+
operationName,
180+
query,
181+
variables,
182+
request,
183+
schema,
184+
parse,
185+
validate,
186+
execute,
187+
contextFactory,
188+
});
189+
190+
if (result.type === 'RESPONSE') {
191+
res.status(result.status);
192+
res.send(result.payload);
193+
} else if (result.type === 'MULTIPART_RESPONSE') {
194+
res.raw.writeHead(200, {
195+
Connection: 'keep-alive',
196+
'Content-Type': 'multipart/mixed; boundary="-"',
197+
'Transfer-Encoding': 'chunked',
198+
});
199+
200+
req.raw.on('close', () => {
201+
result.unsubscribe();
202+
});
203+
204+
res.raw.write('---');
205+
206+
await result.subscribe(result => {
207+
const chunk = Buffer.from(JSON.stringify(result), 'utf8');
208+
const data = [
209+
'',
210+
'Content-Type: application/json; charset=utf-8',
211+
'Content-Length: ' + String(chunk.length),
212+
'',
213+
chunk,
214+
];
215+
216+
if (result.hasNext) {
217+
data.push('---');
218+
}
219+
220+
res.raw.write(data.join('\r\n'));
221+
});
222+
223+
res.raw.write('\r\n-----\r\n');
224+
res.raw.end();
225+
} else {
226+
res.raw.writeHead(200, {
227+
'Content-Type': 'text/event-stream',
228+
Connection: 'keep-alive',
229+
'Cache-Control': 'no-cache',
230+
});
231+
232+
req.raw.on('close', () => {
233+
result.unsubscribe();
234+
});
235+
236+
await result.subscribe(result => {
237+
res.raw.write(`data: ${JSON.stringify(result)}\n\n`);
238+
});
239+
}
240+
}
241+
},
242+
});
243+
244+
app.listen(3000, () => {
245+
console.log(`GraphQL server is running on http://127.0.0.1:3000/graphql`);
246+
});
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@envelop-examples/graphql-helix-defer-stream",
3+
"private": true,
4+
"version": "1.0.0",
5+
"main": "index.js",
6+
"author": "Dotan Simha",
7+
"license": "MIT",
8+
"dependencies": {
9+
"fastify": "3.14.0",
10+
"@envelop/core": "0.2.1",
11+
"graphql-helix": "1.6.1",
12+
"@graphql-tools/schema": "7.1.5",
13+
"graphql": "experimental-stream-defer"
14+
},
15+
"devDependencies": {
16+
"@types/node": "14.14.35",
17+
"ts-node": "9.1.1",
18+
"typescript": "4.3.2"
19+
},
20+
"scripts": {
21+
"start": "ts-node index.ts"
22+
}
23+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"compilerOptions": {
3+
"allowSyntheticDefaultImports": true,
4+
"esModuleInterop": true,
5+
"target": "es2020",
6+
"lib": ["es2020"],
7+
"module": "commonjs",
8+
"moduleResolution": "node",
9+
"sourceMap": true,
10+
"experimentalDecorators": true,
11+
"emitDecoratorMetadata": true,
12+
"outDir": "dist",
13+
"allowUnreachableCode": false,
14+
"allowUnusedLabels": false,
15+
"alwaysStrict": true,
16+
"noImplicitAny": false,
17+
"noImplicitReturns": true,
18+
"noImplicitThis": true,
19+
"noUnusedLocals": false,
20+
"noUnusedParameters": false,
21+
"importHelpers": true,
22+
"skipLibCheck": true
23+
},
24+
"include": ["."],
25+
"exclude": ["node_modules"]
26+
}

0 commit comments

Comments
 (0)