graphql-http 0.1.0 → 1.0.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.
@@ -1,6 +1,6 @@
1
- The MIT License (MIT)
1
+ MIT License
2
2
 
3
- Copyright (c) 2016 Peter M. Elias
3
+ Copyright (c) 2022 Denis Badurina
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,53 +1,666 @@
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, plugable, 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
10
49
 
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'
50
+ ##### With [`http`](https://nodejs.org/api/http.html)
51
+
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');
35
208
  ```
36
209
 
37
- Mutations
210
+ #### Use the client
38
211
 
39
212
  ```js
40
- client.mutate(`
41
- mutation ($id: RecordID!, $name: String!) {
42
- updateUser(input: {id: $id, name: $name}) {
43
- user {
44
- id
45
- name
46
- }
213
+ import { createClient } from 'graphql-http';
214
+
215
+ const client = createClient({
216
+ url: 'http://localhost:4000/graphql',
217
+ });
218
+
219
+ (async () => {
220
+ let cancel = () => {
221
+ /* abort the request if it is in-flight */
222
+ };
223
+
224
+ const result = await new Promise((resolve, reject) => {
225
+ let result;
226
+ cancel = client.subscribe(
227
+ {
228
+ query: '{ hello }',
229
+ },
230
+ {
231
+ next: (data) => (result = data),
232
+ error: reject,
233
+ complete: () => resolve(result),
234
+ },
235
+ );
236
+ });
237
+
238
+ expect(result).toEqual({ hello: 'world' });
239
+ })();
240
+ ```
241
+
242
+ ## Recipes
243
+
244
+ <details id="promise">
245
+ <summary><a href="#promise">🔗</a> Client usage with Promise</summary>
246
+
247
+ ```ts
248
+ import { ExecutionResult } from 'graphql';
249
+ import { createClient, RequestParams } from 'graphql-http';
250
+ import { getSession } from './my-auth';
251
+
252
+ const client = createClient({
253
+ url: 'http://hey.there:4000/graphql',
254
+ headers: async () => {
255
+ const session = await getSession();
256
+ if (session) {
257
+ return {
258
+ Authorization: `Bearer ${session.token}`,
259
+ };
47
260
  }
261
+ },
262
+ });
263
+
264
+ function execute<Data, Extensions>(
265
+ params: RequestParams,
266
+ ): [request: Promise<ExecutionResult<Data, Extensions>>, cancel: () => void] {
267
+ let cancel!: () => void;
268
+ const request = new Promise<ExecutionResult<Data, Extensions>>(
269
+ (resolve, reject) => {
270
+ let result: ExecutionResult<Data, Extensions>;
271
+ cancel = client.subscribe<Data, Extensions>(params, {
272
+ next: (data) => (result = data),
273
+ error: reject,
274
+ complete: () => resolve(result),
275
+ });
276
+ },
277
+ );
278
+ return [request, cancel];
279
+ }
280
+
281
+ (async () => {
282
+ const [request, cancel] = execute({
283
+ query: '{ hello }',
284
+ });
285
+
286
+ // just an example, not a real function
287
+ onUserLeavePage(() => {
288
+ cancel();
289
+ });
290
+
291
+ const result = await request;
292
+
293
+ expect(result).toBe({ data: { hello: 'world' } });
294
+ })();
295
+ ```
296
+
297
+ </details>
298
+
299
+ </details>
300
+
301
+ <details id="observable">
302
+ <summary><a href="#observable">🔗</a> Client usage with <a href="https://github.com/tc39/proposal-observable">Observable</a></summary>
303
+
304
+ ```js
305
+ import { Observable } from 'relay-runtime';
306
+ // or
307
+ import { Observable } from '@apollo/client/core';
308
+ // or
309
+ import { Observable } from 'rxjs';
310
+ // or
311
+ import Observable from 'zen-observable';
312
+ // or any other lib which implements Observables as per the ECMAScript proposal: https://github.com/tc39/proposal-observable
313
+ import { createClient } from 'graphql-http';
314
+ import { getSession } from './my-auth';
315
+
316
+ const client = createClient({
317
+ url: 'http://graphql.loves:4000/observables',
318
+ headers: async () => {
319
+ const session = await getSession();
320
+ if (session) {
321
+ return {
322
+ Authorization: `Bearer ${session.token}`,
323
+ };
324
+ }
325
+ },
326
+ });
327
+
328
+ const observable = new Observable((observer) =>
329
+ client.subscribe({ query: '{ hello }' }, observer),
330
+ );
331
+
332
+ const subscription = observable.subscribe({
333
+ next: (result) => {
334
+ expect(result).toBe({ data: { hello: 'world' } });
335
+ },
336
+ });
337
+
338
+ // unsubscribe will cancel the request if it is pending
339
+ subscription.unsubscribe();
340
+ ```
341
+
342
+ </details>
343
+
344
+ <details id="relay">
345
+ <summary><a href="#relay">🔗</a> Client usage with <a href="https://relay.dev">Relay</a></summary>
346
+
347
+ ```ts
348
+ import { GraphQLError } from 'graphql';
349
+ import {
350
+ Network,
351
+ Observable,
352
+ RequestParameters,
353
+ Variables,
354
+ } from 'relay-runtime';
355
+ import { createClient } from 'graphql-http';
356
+ import { getSession } from './my-auth';
357
+
358
+ const client = createClient({
359
+ url: 'http://i.love:4000/graphql',
360
+ headers: async () => {
361
+ const session = await getSession();
362
+ if (session) {
363
+ return {
364
+ Authorization: `Bearer ${session.token}`,
365
+ };
366
+ }
367
+ },
368
+ });
369
+
370
+ function fetch(operation: RequestParameters, variables: Variables) {
371
+ return Observable.create((sink) => {
372
+ if (!operation.text) {
373
+ return sink.error(new Error('Operation text cannot be empty'));
374
+ }
375
+ return client.subscribe(
376
+ {
377
+ operationName: operation.name,
378
+ query: operation.text,
379
+ variables,
380
+ },
381
+ sink,
382
+ );
383
+ });
384
+ }
385
+
386
+ export const network = Network.create(fetch);
387
+ ```
388
+
389
+ </details>
390
+
391
+ <details id="apollo-client">
392
+ <summary><a href="#apollo-client">🔗</a> Client usage with <a href="https://www.apollographql.com">Apollo</a></summary>
393
+
394
+ ```ts
395
+ import {
396
+ ApolloLink,
397
+ Operation,
398
+ FetchResult,
399
+ Observable,
400
+ } from '@apollo/client/core';
401
+ import { print, GraphQLError } from 'graphql';
402
+ import { createClient, ClientOptions, Client } from 'graphql-http';
403
+ import { getSession } from './my-auth';
404
+
405
+ class HTTPLink extends ApolloLink {
406
+ private client: Client;
407
+
408
+ constructor(options: ClientOptions) {
409
+ super();
410
+ this.client = createClient(options);
48
411
  }
49
- `, { id: 1234, name: 'Danny' }).then((result) => {
50
- console.log(result.data.user);
51
- // => { id: 1234, name: 'Danny' }
412
+
413
+ public request(operation: Operation): Observable<FetchResult> {
414
+ return new Observable((sink) => {
415
+ return this.client.subscribe<FetchResult>(
416
+ { ...operation, query: print(operation.query) },
417
+ {
418
+ next: sink.next.bind(sink),
419
+ complete: sink.complete.bind(sink),
420
+ error: sink.error.bind(sink),
421
+ },
422
+ );
423
+ });
424
+ }
425
+ }
426
+
427
+ const link = new HTTPLink({
428
+ url: 'http://where.is:4000/graphql',
429
+ headers: async () => {
430
+ const session = await getSession();
431
+ if (session) {
432
+ return {
433
+ Authorization: `Bearer ${session.token}`,
434
+ };
435
+ }
436
+ },
52
437
  });
53
438
  ```
439
+
440
+ </details>
441
+
442
+ <details id="request-retries">
443
+ <summary><a href="#request-retries">🔗</a> Client usage with request retries</summary>
444
+
445
+ ```ts
446
+ import { createClient, NetworkError } from 'graphql-http';
447
+
448
+ const client = createClient({
449
+ url: 'http://unstable.service:4000/graphql',
450
+ shouldRetry: async (err: NetworkError, retries: number) => {
451
+ if (retries > 3) {
452
+ // max 3 retries and then report service down
453
+ return false;
454
+ }
455
+
456
+ // try again when service unavailable, could be temporary
457
+ if (err.response?.status === 503) {
458
+ // wait one second (you can alternatively time the promise resolution to your preference)
459
+ await new Promise((resolve) => setTimeout(resolve, 1000));
460
+ return true;
461
+ }
462
+
463
+ // otherwise report error immediately
464
+ return false;
465
+ },
466
+ });
467
+ ```
468
+
469
+ </details>
470
+
471
+ <details id="browser">
472
+ <summary><a href="#browser">🔗</a> Client usage in browser</summary>
473
+
474
+ ```html
475
+ <!DOCTYPE html>
476
+ <html>
477
+ <head>
478
+ <meta charset="utf-8" />
479
+ <title>GraphQL over HTTP</title>
480
+ <script
481
+ type="text/javascript"
482
+ src="https://unpkg.com/graphql-http/umd/graphql-http.min.js"
483
+ ></script>
484
+ </head>
485
+ <body>
486
+ <script type="text/javascript">
487
+ const client = graphqlHttp.createClient({
488
+ url: 'http://umdfor.the:4000/win/graphql',
489
+ });
490
+
491
+ // consider other recipes for usage inspiration
492
+ </script>
493
+ </body>
494
+ </html>
495
+ ```
496
+
497
+ </details>
498
+
499
+ <details id="node-client">
500
+ <summary><a href="#node-client">🔗</a> Client usage in Node</summary>
501
+
502
+ ```js
503
+ const fetch = require('node-fetch'); // yarn add node-fetch
504
+ const { AbortController } = require('node-abort-controller'); // (node < v15) yarn add node-abort-controller
505
+ const { createClient } = require('graphql-http');
506
+
507
+ const client = createClient({
508
+ url: 'http://no.browser:4000/graphql',
509
+ fetchFn: fetch,
510
+ abortControllerImpl: AbortController, // node < v15
511
+ });
512
+
513
+ // consider other recipes for usage inspiration
514
+ ```
515
+
516
+ </details>
517
+
518
+ <details id="auth">
519
+ <summary><a href="#auth">🔗</a> Server handler usage with authentication</summary>
520
+
521
+ 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.
522
+
523
+ ```js
524
+ import { createHandler } from 'graphql-http';
525
+ import {
526
+ schema,
527
+ getUserFromCookies,
528
+ getUserFromAuthorizationHeader,
529
+ } from './my-graphql';
530
+
531
+ const handler = createHandler({
532
+ schema,
533
+ context: async (req) => {
534
+ // process token, authenticate user and attach it to your graphql context
535
+ const userId = await getUserFromCookies(req.headers.cookie);
536
+ // or
537
+ const userId = await getUserFromAuthorizationHeader(
538
+ req.headers.authorization,
539
+ );
540
+
541
+ // respond with 401 if the user was not authenticated
542
+ if (!userId) {
543
+ return [null, { status: 401, statusText: 'Unauthorized' }];
544
+ }
545
+
546
+ // otherwise attach the user to the graphql context
547
+ return { userId };
548
+ },
549
+ });
550
+ ```
551
+
552
+ </details>
553
+
554
+ <details id="context">
555
+ <summary><a href="#context">🔗</a> Server handler usage with custom context value</summary>
556
+
557
+ ```js
558
+ import { createHandler } from 'graphql-http';
559
+ import { schema, getDynamicContext } from './my-graphql';
560
+
561
+ const handler = createHandler({
562
+ schema,
563
+ context: async (req, args) => {
564
+ return getDynamicContext(req, args);
565
+ },
566
+ // or static context by supplying the value direcly
567
+ });
568
+ ```
569
+
570
+ </details>
571
+
572
+ <details id="custom-exec">
573
+ <summary><a href="#custom-exec">🔗</a> Server handler usage with custom execution arguments</summary>
574
+
575
+ ```js
576
+ import { parse } from 'graphql';
577
+ import { createHandler } from 'graphql-http';
578
+ import { getSchemaForRequest, myValidationRules } from './my-graphql';
579
+
580
+ const handler = createHandler({
581
+ onSubscribe: async (req, params) => {
582
+ const schema = await getSchemaForRequest(req);
583
+
584
+ const args = {
585
+ schema,
586
+ operationName: params.operationName,
587
+ document: parse(params.query),
588
+ variableValues: params.variables,
589
+ };
590
+
591
+ return args;
592
+ },
593
+ });
594
+ ```
595
+
596
+ </details>
597
+
598
+ <details id="auth-node-early">
599
+ <summary><a href="#auth-node-early">🔗</a> Server handler usage in Node with early authentication (recommended)</summary>
600
+
601
+ 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.
602
+
603
+ ```js
604
+ import { createHandler } from 'graphql-http';
605
+ import {
606
+ schema,
607
+ getUserFromCookies,
608
+ getUserFromAuthorizationHeader,
609
+ } from './my-graphql';
610
+
611
+ const handler = createHandler({
612
+ schema,
613
+ context: async (req) => {
614
+ // user is authenticated early (see below), simply attach it to the graphql context
615
+ return { userId: req.raw.userId };
616
+ },
617
+ });
618
+
619
+ const server = http.createServer(async (req, res) => {
620
+ if (!req.url.startsWith('/graphql')) {
621
+ return res.writeHead(404).end();
622
+ }
623
+
624
+ try {
625
+ // process token, authenticate user and attach it to the request
626
+ req.userId = await getUserFromCookies(req.headers.cookie);
627
+ // or
628
+ req.userId = await getUserFromAuthorizationHeader(
629
+ req.headers.authorization,
630
+ );
631
+
632
+ // respond with 401 if the user was not authenticated
633
+ if (!req.userId) {
634
+ return res.writeHead(401, 'Unauthorized').end();
635
+ }
636
+
637
+ const [body, init] = await handler({
638
+ url: req.url,
639
+ method: req.method,
640
+ headers: req.headers,
641
+ body: await new Promise((resolve) => {
642
+ let body = '';
643
+ req.on('data', (chunk) => (body += chunk));
644
+ req.on('end', () => resolve(body));
645
+ }),
646
+ raw: req,
647
+ });
648
+ res.writeHead(init.status, init.statusText, init.headers).end(body);
649
+ } catch (err) {
650
+ res.writeHead(500).end(err.message);
651
+ }
652
+ });
653
+
654
+ server.listen(4000);
655
+ console.log('Listening to port 4000');
656
+ ```
657
+
658
+ </details>
659
+
660
+ ## [Documentation](docs/)
661
+
662
+ Check the [docs folder](docs/) out for [TypeDoc](https://typedoc.org) generated documentation.
663
+
664
+ ## [Want to help?](CONTRIBUTING.md)
665
+
666
+ 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!