graphql-http 0.1.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,53 +1,716 @@
1
- # graphql-http
1
+ <div align="center">
2
+ <br />
2
3
 
3
- A GraphQL client for executing queries over HTTP.
4
+ <h3>graphql-http</h3>
4
5
 
6
+ <h6>Simple, pluggable, zero-dependency, <a href="https://graphql.github.io/graphql-over-http">GraphQL over HTTP Protocol</a> compliant server and client.</h6>
5
7
 
6
- ### Usage
8
+ [![Continuous integration](https://github.com/enisdenjo/graphql-http/workflows/Continuous%20integration/badge.svg)](https://github.com/enisdenjo/graphql-http/actions?query=workflow%3A%22Continuous+integration%22) [![graphql-http](https://img.shields.io/npm/v/graphql-http.svg?label=graphql-http&logo=npm)](https://www.npmjs.com/package/graphql-http)
9
+
10
+ <i>Need subscriptions? Try <b>[graphql-ws](https://github.com/enisdenjo/graphql-ws)</b> or <b>[graphql-sse](https://github.com/enisdenjo/graphql-sse)</b> instead!</i>
11
+
12
+ <br />
13
+ </div>
14
+
15
+ ## Getting started
16
+
17
+ #### Install
18
+
19
+ ```shell
20
+ yarn add graphql-http
21
+ ```
22
+
23
+ #### Create a GraphQL schema
7
24
 
8
25
  ```js
9
- import { GQLClient } from 'graphql-http';
26
+ import { GraphQLSchema, GraphQLObjectType, GraphQLString } from 'graphql';
27
+
28
+ /**
29
+ * Construct a GraphQL schema and define the necessary resolvers.
30
+ *
31
+ * type Query {
32
+ * hello: String
33
+ * }
34
+ */
35
+ const schema = new GraphQLSchema({
36
+ query: new GraphQLObjectType({
37
+ name: 'Query',
38
+ fields: {
39
+ hello: {
40
+ type: GraphQLString,
41
+ resolve: () => 'world',
42
+ },
43
+ },
44
+ }),
45
+ });
46
+ ```
47
+
48
+ #### Start the server
49
+
50
+ ##### With [`http`](https://nodejs.org/api/http.html)
10
51
 
11
- const client = GQLClient('http://localhost:3000', {
12
- // anything passed here is merged with
13
- // the options passed to fetch()
14
- credentials: true,
15
- headers: {
16
- 'X-Requested-With': 'XMLHttpRequest'
52
+ ```js
53
+ import http from 'http';
54
+ import { createHandler } from 'graphql-http';
55
+ import { schema } from './previous-step';
56
+
57
+ // Create the GraphQL over HTTP handler
58
+ const handler = createHandler({ schema });
59
+
60
+ // Create a HTTP server using the handler on `/graphql`
61
+ const server = http.createServer(async (req, res) => {
62
+ if (!req.url.startsWith('/graphql')) {
63
+ return res.writeHead(404).end();
64
+ }
65
+
66
+ try {
67
+ const [body, init] = await handler({
68
+ url: req.url,
69
+ method: req.method,
70
+ headers: req.headers,
71
+ body: await new Promise((resolve) => {
72
+ let body = '';
73
+ req.on('data', (chunk) => (body += chunk));
74
+ req.on('end', () => resolve(body));
75
+ }),
76
+ raw: req,
77
+ });
78
+ res.writeHead(init.status, init.statusText, init.headers).end(body);
79
+ } catch (err) {
80
+ res.writeHead(500).end(err.message);
17
81
  }
18
82
  });
83
+
84
+ server.listen(4000);
85
+ console.log('Listening to port 4000');
19
86
  ```
20
87
 
21
- Queries
88
+ ##### With [`http2`](https://nodejs.org/api/http2.html)
89
+
90
+ _Browsers might complain about self-signed SSL/TLS certificates. [Help can be found on StackOverflow.](https://stackoverflow.com/questions/7580508/getting-chrome-to-accept-self-signed-localhost-certificate)_
91
+
92
+ ```shell
93
+ $ openssl req -x509 -newkey rsa:2048 -nodes -sha256 -subj '/CN=localhost' \
94
+ -keyout localhost-privkey.pem -out localhost-cert.pem
95
+ ```
22
96
 
23
97
  ```js
24
- client.query(`
25
- query ($id: RecordID!) {
26
- user(id: $id) {
27
- id
28
- name
98
+ import fs from 'fs';
99
+ import http2 from 'http2';
100
+ import { createHandler } from 'graphql-http';
101
+ import { schema } from './previous-step';
102
+
103
+ // Create the GraphQL over HTTP handler
104
+ const handler = createHandler({ schema });
105
+
106
+ // Create a HTTP/2 server using the handler on `/graphql`
107
+ const server = http2.createSecureServer(
108
+ {
109
+ key: fs.readFileSync('localhost-privkey.pem'),
110
+ cert: fs.readFileSync('localhost-cert.pem'),
111
+ },
112
+ async (req, res) => {
113
+ if (!req.url.startsWith('/graphql')) {
114
+ return res.writeHead(404).end();
29
115
  }
116
+
117
+ try {
118
+ const [body, init] = await handler({
119
+ url: req.url,
120
+ method: req.method,
121
+ headers: req.headers,
122
+ body: await new Promise((resolve) => {
123
+ let body = '';
124
+ req.on('data', (chunk) => (body += chunk));
125
+ req.on('end', () => resolve(body));
126
+ }),
127
+ raw: req,
128
+ });
129
+ res.writeHead(init.status, init.statusText, init.headers).end(body);
130
+ } catch (err) {
131
+ res.writeHead(500).end(err.message);
132
+ }
133
+ },
134
+ );
135
+
136
+ server.listen(4000);
137
+ console.log('Listening to port 4000');
138
+ ```
139
+
140
+ ##### With [`express`](https://expressjs.com/)
141
+
142
+ ```js
143
+ import express from 'express'; // yarn add express
144
+ import { createHandler } from 'graphql-http';
145
+ import { schema } from './previous-step';
146
+
147
+ // Create the GraphQL over HTTP handler
148
+ const handler = createHandler({ schema });
149
+
150
+ // Create an express app serving all methods on `/graphql`
151
+ const app = express();
152
+ app.use('/graphql', async (req, res) => {
153
+ try {
154
+ const [body, init] = await handler({
155
+ url: req.url,
156
+ method: req.method,
157
+ headers: req.headers,
158
+ body: await new Promise((resolve) => {
159
+ let body = '';
160
+ req.on('data', (chunk) => (body += chunk));
161
+ req.on('end', () => resolve(body));
162
+ }),
163
+ raw: req,
164
+ });
165
+ res.writeHead(init.status, init.statusText, init.headers).end(body);
166
+ } catch (err) {
167
+ res.writeHead(500).end(err.message);
168
+ }
169
+ });
170
+
171
+ app.listen(4000);
172
+ console.log('Listening to port 4000');
173
+ ```
174
+
175
+ ##### With [`fastify`](https://www.fastify.io/)
176
+
177
+ ```js
178
+ import Fastify from 'fastify'; // yarn add fastify
179
+ import { createHandler } from 'graphql-http';
180
+ import { schema } from './previous-step';
181
+
182
+ // Create the GraphQL over HTTP handler
183
+ const handler = createHandler({ schema });
184
+
185
+ // Create a fastify instance serving all methods on `/graphql`
186
+ const fastify = Fastify();
187
+ fastify.all('/graphql', async (req, res) => {
188
+ try {
189
+ const [body, init] = await handler({
190
+ url: req.url,
191
+ method: req.method,
192
+ headers: req.headers,
193
+ body: await new Promise((resolve) => {
194
+ let body = '';
195
+ req.on('data', (chunk) => (body += chunk));
196
+ req.on('end', () => resolve(body));
197
+ }),
198
+ raw: req,
199
+ });
200
+ res.writeHead(init.status, init.statusText, init.headers).end(body);
201
+ } catch (err) {
202
+ res.writeHead(500).end(err.message);
30
203
  }
31
- `, { id: 1234 }).then((result) => {
32
- console.log(result.data.user);
33
- // => { id: 1234, name: ... }
34
204
  });
205
+
206
+ fastify.listen(4000);
207
+ console.log('Listening to port 4000');
208
+ ```
209
+
210
+ ##### With [`Deno`](https://deno.land/)
211
+
212
+ ```ts
213
+ import { serve } from 'https://deno.land/std@0.151.0/http/server.ts';
214
+ import { createHandler } from 'https://esm.sh/graphql-http';
215
+ import { schema } from './previous-step';
216
+
217
+ const handler = createHandler<Request>({ schema });
218
+
219
+ await serve(
220
+ async (req: Request) => {
221
+ const [path, _search] = req.url.split('?');
222
+ if (!path.endsWith('/graphql')) {
223
+ return new Response(null, { status: 404, statusText: 'Not Found' });
224
+ }
225
+
226
+ const headers: Record<string, string> = {};
227
+ req.headers.forEach((value, key) => (headers[key] = value));
228
+ const [body, init] = await handler({
229
+ url: req.url,
230
+ method: req.method,
231
+ headers,
232
+ body: await req.text(),
233
+ raw: req,
234
+ });
235
+ return new Response(body, init);
236
+ },
237
+ {
238
+ port: 4000,
239
+ },
240
+ );
241
+
242
+ // Listening to port 4000
243
+ ```
244
+
245
+ #### Use the client
246
+
247
+ ```js
248
+ import { createClient } from 'graphql-http';
249
+
250
+ const client = createClient({
251
+ url: 'http://localhost:4000/graphql',
252
+ });
253
+
254
+ (async () => {
255
+ let cancel = () => {
256
+ /* abort the request if it is in-flight */
257
+ };
258
+
259
+ const result = await new Promise((resolve, reject) => {
260
+ let result;
261
+ cancel = client.subscribe(
262
+ {
263
+ query: '{ hello }',
264
+ },
265
+ {
266
+ next: (data) => (result = data),
267
+ error: reject,
268
+ complete: () => resolve(result),
269
+ },
270
+ );
271
+ });
272
+
273
+ expect(result).toEqual({ hello: 'world' });
274
+ })();
35
275
  ```
36
276
 
37
- Mutations
277
+ ## Recipes
278
+
279
+ <details id="promise">
280
+ <summary><a href="#promise">🔗</a> Client usage with Promise</summary>
281
+
282
+ ```ts
283
+ import { ExecutionResult } from 'graphql';
284
+ import { createClient, RequestParams } from 'graphql-http';
285
+ import { getSession } from './my-auth';
286
+
287
+ const client = createClient({
288
+ url: 'http://hey.there:4000/graphql',
289
+ headers: async () => {
290
+ const session = await getSession();
291
+ if (session) {
292
+ return {
293
+ Authorization: `Bearer ${session.token}`,
294
+ };
295
+ }
296
+ },
297
+ });
298
+
299
+ function execute<Data, Extensions>(
300
+ params: RequestParams,
301
+ ): [request: Promise<ExecutionResult<Data, Extensions>>, cancel: () => void] {
302
+ let cancel!: () => void;
303
+ const request = new Promise<ExecutionResult<Data, Extensions>>(
304
+ (resolve, reject) => {
305
+ let result: ExecutionResult<Data, Extensions>;
306
+ cancel = client.subscribe<Data, Extensions>(params, {
307
+ next: (data) => (result = data),
308
+ error: reject,
309
+ complete: () => resolve(result),
310
+ });
311
+ },
312
+ );
313
+ return [request, cancel];
314
+ }
315
+
316
+ (async () => {
317
+ const [request, cancel] = execute({
318
+ query: '{ hello }',
319
+ });
320
+
321
+ // just an example, not a real function
322
+ onUserLeavePage(() => {
323
+ cancel();
324
+ });
325
+
326
+ const result = await request;
327
+
328
+ expect(result).toBe({ data: { hello: 'world' } });
329
+ })();
330
+ ```
331
+
332
+ </details>
333
+
334
+ </details>
335
+
336
+ <details id="observable">
337
+ <summary><a href="#observable">🔗</a> Client usage with <a href="https://github.com/tc39/proposal-observable">Observable</a></summary>
38
338
 
39
339
  ```js
40
- client.mutate(`
41
- mutation ($id: RecordID!, $name: String!) {
42
- updateUser(input: {id: $id, name: $name}) {
43
- user {
44
- id
45
- name
46
- }
340
+ import { Observable } from 'relay-runtime';
341
+ // or
342
+ import { Observable } from '@apollo/client/core';
343
+ // or
344
+ import { Observable } from 'rxjs';
345
+ // or
346
+ import Observable from 'zen-observable';
347
+ // or any other lib which implements Observables as per the ECMAScript proposal: https://github.com/tc39/proposal-observable
348
+ import { createClient } from 'graphql-http';
349
+ import { getSession } from './my-auth';
350
+
351
+ const client = createClient({
352
+ url: 'http://graphql.loves:4000/observables',
353
+ headers: async () => {
354
+ const session = await getSession();
355
+ if (session) {
356
+ return {
357
+ Authorization: `Bearer ${session.token}`,
358
+ };
359
+ }
360
+ },
361
+ });
362
+
363
+ const observable = new Observable((observer) =>
364
+ client.subscribe({ query: '{ hello }' }, observer),
365
+ );
366
+
367
+ const subscription = observable.subscribe({
368
+ next: (result) => {
369
+ expect(result).toBe({ data: { hello: 'world' } });
370
+ },
371
+ });
372
+
373
+ // unsubscribe will cancel the request if it is pending
374
+ subscription.unsubscribe();
375
+ ```
376
+
377
+ </details>
378
+
379
+ <details id="relay">
380
+ <summary><a href="#relay">🔗</a> Client usage with <a href="https://relay.dev">Relay</a></summary>
381
+
382
+ ```ts
383
+ import { GraphQLError } from 'graphql';
384
+ import {
385
+ Network,
386
+ Observable,
387
+ RequestParameters,
388
+ Variables,
389
+ } from 'relay-runtime';
390
+ import { createClient } from 'graphql-http';
391
+ import { getSession } from './my-auth';
392
+
393
+ const client = createClient({
394
+ url: 'http://i.love:4000/graphql',
395
+ headers: async () => {
396
+ const session = await getSession();
397
+ if (session) {
398
+ return {
399
+ Authorization: `Bearer ${session.token}`,
400
+ };
47
401
  }
402
+ },
403
+ });
404
+
405
+ function fetch(operation: RequestParameters, variables: Variables) {
406
+ return Observable.create((sink) => {
407
+ if (!operation.text) {
408
+ return sink.error(new Error('Operation text cannot be empty'));
409
+ }
410
+ return client.subscribe(
411
+ {
412
+ operationName: operation.name,
413
+ query: operation.text,
414
+ variables,
415
+ },
416
+ sink,
417
+ );
418
+ });
419
+ }
420
+
421
+ export const network = Network.create(fetch);
422
+ ```
423
+
424
+ </details>
425
+
426
+ <details id="apollo-client">
427
+ <summary><a href="#apollo-client">🔗</a> Client usage with <a href="https://www.apollographql.com">Apollo</a></summary>
428
+
429
+ ```ts
430
+ import {
431
+ ApolloLink,
432
+ Operation,
433
+ FetchResult,
434
+ Observable,
435
+ } from '@apollo/client/core';
436
+ import { print, GraphQLError } from 'graphql';
437
+ import { createClient, ClientOptions, Client } from 'graphql-http';
438
+ import { getSession } from './my-auth';
439
+
440
+ class HTTPLink extends ApolloLink {
441
+ private client: Client;
442
+
443
+ constructor(options: ClientOptions) {
444
+ super();
445
+ this.client = createClient(options);
446
+ }
447
+
448
+ public request(operation: Operation): Observable<FetchResult> {
449
+ return new Observable((sink) => {
450
+ return this.client.subscribe<FetchResult>(
451
+ { ...operation, query: print(operation.query) },
452
+ {
453
+ next: sink.next.bind(sink),
454
+ complete: sink.complete.bind(sink),
455
+ error: sink.error.bind(sink),
456
+ },
457
+ );
458
+ });
48
459
  }
49
- `, { id: 1234, name: 'Danny' }).then((result) => {
50
- console.log(result.data.user);
51
- // => { id: 1234, name: 'Danny' }
460
+ }
461
+
462
+ const link = new HTTPLink({
463
+ url: 'http://where.is:4000/graphql',
464
+ headers: async () => {
465
+ const session = await getSession();
466
+ if (session) {
467
+ return {
468
+ Authorization: `Bearer ${session.token}`,
469
+ };
470
+ }
471
+ },
52
472
  });
53
473
  ```
474
+
475
+ </details>
476
+
477
+ <details id="request-retries">
478
+ <summary><a href="#request-retries">🔗</a> Client usage with request retries</summary>
479
+
480
+ ```ts
481
+ import { createClient, NetworkError } from 'graphql-http';
482
+
483
+ const client = createClient({
484
+ url: 'http://unstable.service:4000/graphql',
485
+ shouldRetry: async (err: NetworkError, retries: number) => {
486
+ if (retries > 3) {
487
+ // max 3 retries and then report service down
488
+ return false;
489
+ }
490
+
491
+ // try again when service unavailable, could be temporary
492
+ if (err.response?.status === 503) {
493
+ // wait one second (you can alternatively time the promise resolution to your preference)
494
+ await new Promise((resolve) => setTimeout(resolve, 1000));
495
+ return true;
496
+ }
497
+
498
+ // otherwise report error immediately
499
+ return false;
500
+ },
501
+ });
502
+ ```
503
+
504
+ </details>
505
+
506
+ <details id="browser">
507
+ <summary><a href="#browser">🔗</a> Client usage in browser</summary>
508
+
509
+ ```html
510
+ <!DOCTYPE html>
511
+ <html>
512
+ <head>
513
+ <meta charset="utf-8" />
514
+ <title>GraphQL over HTTP</title>
515
+ <script
516
+ type="text/javascript"
517
+ src="https://unpkg.com/graphql-http/umd/graphql-http.min.js"
518
+ ></script>
519
+ </head>
520
+ <body>
521
+ <script type="text/javascript">
522
+ const client = graphqlHttp.createClient({
523
+ url: 'http://umdfor.the:4000/win/graphql',
524
+ });
525
+
526
+ // consider other recipes for usage inspiration
527
+ </script>
528
+ </body>
529
+ </html>
530
+ ```
531
+
532
+ </details>
533
+
534
+ <details id="node-client">
535
+ <summary><a href="#node-client">🔗</a> Client usage in Node</summary>
536
+
537
+ ```js
538
+ const fetch = require('node-fetch'); // yarn add node-fetch
539
+ const { AbortController } = require('node-abort-controller'); // (node < v15) yarn add node-abort-controller
540
+ const { createClient } = require('graphql-http');
541
+
542
+ const client = createClient({
543
+ url: 'http://no.browser:4000/graphql',
544
+ fetchFn: fetch,
545
+ abortControllerImpl: AbortController, // node < v15
546
+ });
547
+
548
+ // consider other recipes for usage inspiration
549
+ ```
550
+
551
+ </details>
552
+
553
+ <details id="deno-client">
554
+ <summary><a href="#deno-client">🔗</a> Client usage in Deno</summary>
555
+
556
+ ```js
557
+ import { createClient } from 'graphql-http';
558
+
559
+ const client = createClient({
560
+ url: 'http://deno.earth:4000/graphql',
561
+ });
562
+
563
+ // consider other recipes for usage inspiration
564
+ ```
565
+
566
+ </details>
567
+
568
+ <details id="auth">
569
+ <summary><a href="#auth">🔗</a> Server handler usage with authentication</summary>
570
+
571
+ Authenticate the user within `graphql-http` during GraphQL execution context assembly. This is a approach is less safe compared to early authentication ([see early authentication in Node](#auth-node-early)) because some GraphQL preparations or operations are executed even if the user is not unauthorized.
572
+
573
+ ```js
574
+ import { createHandler } from 'graphql-http';
575
+ import {
576
+ schema,
577
+ getUserFromCookies,
578
+ getUserFromAuthorizationHeader,
579
+ } from './my-graphql';
580
+
581
+ const handler = createHandler({
582
+ schema,
583
+ context: async (req) => {
584
+ // process token, authenticate user and attach it to your graphql context
585
+ const userId = await getUserFromCookies(req.headers.cookie);
586
+ // or
587
+ const userId = await getUserFromAuthorizationHeader(
588
+ req.headers.authorization,
589
+ );
590
+
591
+ // respond with 401 if the user was not authenticated
592
+ if (!userId) {
593
+ return [null, { status: 401, statusText: 'Unauthorized' }];
594
+ }
595
+
596
+ // otherwise attach the user to the graphql context
597
+ return { userId };
598
+ },
599
+ });
600
+ ```
601
+
602
+ </details>
603
+
604
+ <details id="context">
605
+ <summary><a href="#context">🔗</a> Server handler usage with custom context value</summary>
606
+
607
+ ```js
608
+ import { createHandler } from 'graphql-http';
609
+ import { schema, getDynamicContext } from './my-graphql';
610
+
611
+ const handler = createHandler({
612
+ schema,
613
+ context: async (req, args) => {
614
+ return getDynamicContext(req, args);
615
+ },
616
+ // or static context by supplying the value direcly
617
+ });
618
+ ```
619
+
620
+ </details>
621
+
622
+ <details id="custom-exec">
623
+ <summary><a href="#custom-exec">🔗</a> Server handler usage with custom execution arguments</summary>
624
+
625
+ ```js
626
+ import { parse } from 'graphql';
627
+ import { createHandler } from 'graphql-http';
628
+ import { getSchemaForRequest, myValidationRules } from './my-graphql';
629
+
630
+ const handler = createHandler({
631
+ onSubscribe: async (req, params) => {
632
+ const schema = await getSchemaForRequest(req);
633
+
634
+ const args = {
635
+ schema,
636
+ operationName: params.operationName,
637
+ document: parse(params.query),
638
+ variableValues: params.variables,
639
+ };
640
+
641
+ return args;
642
+ },
643
+ });
644
+ ```
645
+
646
+ </details>
647
+
648
+ <details id="auth-node-early">
649
+ <summary><a href="#auth-node-early">🔗</a> Server handler usage in Node with early authentication (recommended)</summary>
650
+
651
+ Authenticate the user early, before reaching `graphql-http`. This is the recommended approach because no GraphQL preparations or operations are executed if the user is not authorized.
652
+
653
+ ```js
654
+ import { createHandler } from 'graphql-http';
655
+ import {
656
+ schema,
657
+ getUserFromCookies,
658
+ getUserFromAuthorizationHeader,
659
+ } from './my-graphql';
660
+
661
+ const handler = createHandler({
662
+ schema,
663
+ context: async (req) => {
664
+ // user is authenticated early (see below), simply attach it to the graphql context
665
+ return { userId: req.raw.userId };
666
+ },
667
+ });
668
+
669
+ const server = http.createServer(async (req, res) => {
670
+ if (!req.url.startsWith('/graphql')) {
671
+ return res.writeHead(404).end();
672
+ }
673
+
674
+ try {
675
+ // process token, authenticate user and attach it to the request
676
+ req.userId = await getUserFromCookies(req.headers.cookie);
677
+ // or
678
+ req.userId = await getUserFromAuthorizationHeader(
679
+ req.headers.authorization,
680
+ );
681
+
682
+ // respond with 401 if the user was not authenticated
683
+ if (!req.userId) {
684
+ return res.writeHead(401, 'Unauthorized').end();
685
+ }
686
+
687
+ const [body, init] = await handler({
688
+ url: req.url,
689
+ method: req.method,
690
+ headers: req.headers,
691
+ body: await new Promise((resolve) => {
692
+ let body = '';
693
+ req.on('data', (chunk) => (body += chunk));
694
+ req.on('end', () => resolve(body));
695
+ }),
696
+ raw: req,
697
+ });
698
+ res.writeHead(init.status, init.statusText, init.headers).end(body);
699
+ } catch (err) {
700
+ res.writeHead(500).end(err.message);
701
+ }
702
+ });
703
+
704
+ server.listen(4000);
705
+ console.log('Listening to port 4000');
706
+ ```
707
+
708
+ </details>
709
+
710
+ ## [Documentation](docs/)
711
+
712
+ Check the [docs folder](docs/) out for [TypeDoc](https://typedoc.org) generated documentation.
713
+
714
+ ## [Want to help?](CONTRIBUTING.md)
715
+
716
+ File a bug, contribute with code, or improve documentation? Read up on our guidelines for [contributing](CONTRIBUTING.md) and drive development with `yarn test --watch` away!