File Coverage

blib/lib/Mojolicious/Plugin/GraphQL.pm
Criterion Covered Total %
statement 154 159 96.8
branch 42 54 77.7
condition 11 15 73.3
subroutine 30 30 100.0
pod 1 2 50.0
total 238 260 91.5


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::GraphQL;
2             # ABSTRACT: a plugin for adding GraphQL route handlers
3 1     1   64113 use strict;
  4         19  
  1         33  
4 1     1   6 use warnings;
  1         2  
  1         44  
5 1     1   5 use Mojo::Base 'Mojolicious::Plugin';
  1         3  
  1         8  
6 1     1   237 use Mojo::JSON qw(decode_json to_json);
  1         3  
  1         73  
7 1     1   648 use GraphQL::Execution qw(execute);
  1         182326  
  1         78  
8 1     1   9 use GraphQL::Type::Library -all;
  1         3  
  1         9  
9 1     1   14257 use GraphQL::Debug qw(_debug);
  1         3  
  1         51  
10 1     1   8 use Module::Runtime qw(require_module);
  1         3  
  1         10  
11 1     1   53 use Mojo::Promise;
  1         3  
  1         18  
12 1     1   870 use curry;
  1         349  
  1         39  
13 1     1   7 use Exporter 'import';
  1         2  
  1         76  
14              
15             our $VERSION = '0.19';
16             our @EXPORT_OK = qw(promise_code);
17              
18 1     1   6 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  1         3  
  1         180  
19             use constant promise_code => +{
20             all => sub {
21             # current Mojo::Promise->all only works on promises, force that
22 1 50       5377 my @promises = map is_Promise($_)
23             ? $_ : Mojo::Promise->new->resolve($_),
24             @_;
25             # only actually works when first promise-instance is a
26             # Mojo::Promise, so force it to be one. hoping will be fixed soon
27 1         68 Mojo::Promise->all(@promises);
28             },
29 1         17 resolve => Mojo::Promise->curry::resolve,
30             reject => Mojo::Promise->curry::reject,
31             new => Mojo::Promise->curry::new,
32 1     1   13 };
  1         16  
33             # from https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/message-types.ts
34 1         135 use constant ws_protocol => +{
35             # no legacy ones like 'init'
36             GQL_CONNECTION_INIT => 'connection_init', # Client -> Server
37             GQL_CONNECTION_ACK => 'connection_ack', # Server -> Client
38             GQL_CONNECTION_ERROR => 'connection_error', # Server -> Client
39             # NOTE: The keep alive message type does not follow the standard due to connection optimizations
40             GQL_CONNECTION_KEEP_ALIVE => 'ka', # Server -> Client
41             GQL_CONNECTION_TERMINATE => 'connection_terminate', # Client -> Server
42             GQL_START => 'start', # Client -> Server
43             GQL_DATA => 'data', # Server -> Client
44             GQL_ERROR => 'error', # Server -> Client
45             GQL_COMPLETE => 'complete', # Server -> Client
46             GQL_STOP => 'stop', # Client -> Server
47 1     1   150 };
  1         3  
48              
49             my @DEFAULT_METHODS = qw(get post);
50 1     1   8 use constant EXECUTE => sub { $_[7] = promise_code(); goto &execute; };
  1         5  
  1         130  
  3         76  
  1         8  
51             use constant SUBSCRIBE => sub {
52 3         20 splice @_, 7, 1, promise_code();
53 4         18 goto &GraphQL::Subscription::subscribe;
54 1     1   3 };
  1         2608  
  3         100  
55             sub make_code_closure {
56 4     4 0 32 my ($schema, $root_value, $field_resolver) = @_;
57             sub {
58 5     5   26 my ($c, $body, $execute, $subscribe_resolver) = @_;
59             $execute->(
60             $schema,
61             $body->{query},
62             $root_value,
63             $c->req->headers,
64             $body->{variables},
65             $body->{operationName},
66 12 100       291 $field_resolver,
67             $subscribe_resolver ? (undef, $subscribe_resolver) : (),
68             );
69 5         20 };
70             }
71              
72             sub _safe_serialize {
73 1   100 12   9 my $data = shift // return 'undefined';
74 1         36 my $json = to_json($data);
75 1         4 $json =~ s#/#\\/#g;
76 2         6 return $json;
77             }
78              
79             sub _graphiql_wrap {
80 2     2   11 my ($wrappee, $use_subscription) = @_;
81             sub {
82 4     4   14 my ($c) = @_;
83 3 100 100     415 if (
      66        
84             # not as ignores Firefox-sent multi-accept: $c->accepts('', 'html') and
85             ($c->req->headers->header('Accept')//'') =~ /^text\/html\b/ and
86             !defined $c->req->query_params->param('raw')
87             ) {
88 3         77 my $p = $c->req->query_params;
89 3         93 my $https = $c->req->is_secure;
90 1 50       33 return $c->render(
    100          
91             template => 'graphiql',
92             layout => undef,
93             title => 'GraphiQL',
94             graphiql_version => 'latest',
95             queryString => _safe_serialize( $p->param('query') ),
96             operationName => _safe_serialize( $p->param('operationName') ),
97             resultString => _safe_serialize( $p->param('result') ),
98             variablesString => _safe_serialize( $p->param('variables') ),
99             subscriptionEndpoint => to_json(
100             # if serialises to true (which empty-string will), turns on subs code
101             $use_subscription
102             ? $c->url_for->to_abs->scheme($https ? 'wss' : 'ws')
103             : 0
104             ),
105             );
106             }
107 12         175 goto $wrappee;
108 4         66951 };
109             }
110              
111             sub _decode {
112 12     12   36 my ($bytes) = @_;
113 12         53 my $body = eval { decode_json($bytes) };
  12         2964  
114             # conceal error info like versions from attackers
115 11 100       41 return (0, { errors => [ { message => "Malformed request" } ] }) if $@;
116 7         25 (1, $body);
117             }
118              
119             sub _execute {
120 7     7   15 my ($c, $body, $handler, $execute, $subscribe_fn) = @_;
121 7         23 my $data = eval { $handler->($c, $body, $execute, $subscribe_fn) };
  7         212020  
122 6 100       23 return { errors => [ { message => $@ } ] } if $@;
123 6         20 $data;
124             }
125              
126             sub _make_route_handler {
127 6     6   31 my ($handler) = @_;
128             sub {
129 5     5   33 my ($c) = @_;
130 5         36 my ($decode_ok, $body) = _decode($c->req->body);
131 4 100       20 return $c->render(json => $body) if !$decode_ok;
132 4         23 my $data = _execute($c, $body, $handler, EXECUTE());
133 1 100       17 return $c->render(json => $data) if !is_Promise($data);
134 1         501728 $data->then(sub { $c->render(json => shift) });
  3         10  
135 5         31666 };
136             }
137              
138             sub _make_connection_handler {
139 3     3   32 my ($handler, $subscribe_resolver, $context) = @_;
140             sub {
141 7     7   27 my ($c, $bytes) = @_;
142 7         24 my ($decode_ok, $body) = _decode($bytes);
143             return $c->send({json => {
144             payload => $body,
145             type => ($context->{connected}
146 7 0       22 ? ws_protocol->{GQL_ERROR} : ws_protocol->{GQL_CONNECTION_ERROR}),
    50          
147             }}) if !$decode_ok;
148 7         41 my $msg_type = $body->{type};
149 3 100       11 if ($msg_type eq ws_protocol->{GQL_CONNECTION_INIT}) {
    100          
    50          
150 3         25 $context->{connected} = 1;
151             $c->send({json => {
152             type => ws_protocol->{GQL_CONNECTION_ACK}
153 3         657 }});
154 1 100       4 if ($context->{keepalive}) {
155 1         6 my $cb;
156             $cb = sub {
157 2 50 33     55 return unless $c->tx and $context->{keepalive};
158             $c->send({json => {
159             type => ws_protocol->{GQL_CONNECTION_KEEP_ALIVE},
160 2         648 }});
161 1         4 Mojo::IOLoop->timer($context->{keepalive} => $cb);
162 2         63469 };
163 3         74 $cb->();
164             }
165 3         12 return;
166             } elsif ($msg_type eq ws_protocol->{GQL_START}) {
167 3         15 $context->{id} = $body->{id};
168             my $data = _execute(
169 3         21 $c, $body->{payload}, $handler, SUBSCRIBE(), $subscribe_resolver,
170             );
171             return $c->send({json => {
172             payload => $data, type => ws_protocol->{GQL_ERROR},
173 3 50       77 }}) if !is_Promise($data);
174             $data->then(
175             sub {
176 3         12 my ($result) = @_;
177 0 50       0 if (!is_AsyncIterator($result)) {
178             # subscription error
179             $c->send({json => {
180             payload => $result, type => ws_protocol->{GQL_ERROR},
181             id => $context->{id},
182 0         0 }});
183 0         0 $c->finish;
184 3         28 return;
185             }
186 3         27 my $promise;
187             $context->{async_iterator} = $result->map_then(sub {
188 5         72 DEBUG and _debug('MLPlugin.ai_cb', $context, @_);
189             $c->send({json => {
190             payload => $_[0],
191             type => ws_protocol->{GQL_DATA},
192             id => $context->{id},
193 5         1903 }});
194 5         1681 $promise = $context->{async_iterator}->next_p;
195             $c->send({json => {
196             type => ws_protocol->{GQL_COMPLETE},
197             id => $context->{id},
198 3 100       114 }}) if !$promise; # exhausted, tell client
199 5         209617 });
200 0         0 $promise = $context->{async_iterator}->next_p; # start the process
201             },
202             sub {
203             $c->send({json => {
204             payload => $_[0], type => ws_protocol->{GQL_ERROR},
205             id => $context->{id},
206 0         0 }});
207 1         9 $c->finish;
208             },
209 3         1028 );
210             } elsif ($msg_type eq ws_protocol->{GQL_STOP}) {
211             $c->send({json => {
212             type => ws_protocol->{GQL_COMPLETE},
213             id => $context->{id},
214 1         243 }});
215 1 50       86 $context->{async_iterator}->close_tap if $context->{async_iterator};
216 2         7 undef %$context; # relinquish our refcounts
217             }
218             }
219 7         30517 }
220              
221             sub _make_subs_route_handler {
222 2     2   1677 my ($handler, $subscribe_resolver, $keepalive) = @_;
223 2         72800 require GraphQL::Subscription;
224             sub {
225 3     3   13 my ($c) = @_;
226             # without this, GraphiQL won't accept is valid
227 3         89 my $sec_websocket_protocol = $c->tx->req->headers->sec_websocket_protocol;
228 3 50       14 $c->tx->res->headers->sec_websocket_protocol($sec_websocket_protocol)
229             if $sec_websocket_protocol;
230 3         27 my %context = (keepalive => $keepalive);
231 3         666 $c->on(text => _make_connection_handler($handler, $subscribe_resolver, \%context));
232             $c->on(finish => sub {
233 3 100       157 $context{async_iterator}->close_tap if $context{async_iterator};
234 6         30853 undef %context; # relinquish our refcounts
235 3         14582 });
236 3         36431 };
237             }
238              
239             sub register {
240 6     6 1 28 my ($self, $app, $conf) = @_;
241 3 100       8 if ($conf->{convert}) {
242 3         13 my ($class, @values) = @{ $conf->{convert} };
  3         11  
243 3         22 $class = "GraphQL::Plugin::Convert::$class";
244 3         3917 require_module $class;
245 3         140971 my $converted = $class->to_graphql(@values);
246 6         31 $conf = { %$conf, %$converted };
247             }
248 6 50       30 die "Need schema\n" if !$conf->{schema};
249 6   100     29 my $endpoint = $conf->{endpoint} || '/graphql';
250             my $handler = $conf->{handler} || make_code_closure(
251 6   66     15 @{$conf}{qw(schema root_value resolver)}
252             );
253 6         38 push @{$app->renderer->classes}, __PACKAGE__
254 1 100       30 unless grep $_ eq __PACKAGE__, @{$app->renderer->classes};
  6         88  
255 6         35 my $route_handler = _make_route_handler($handler);
256             $route_handler = _graphiql_wrap($route_handler, $conf->{schema}->subscription)
257 6 100       28 if $conf->{graphiql};
258 6         52 my $r = $app->routes;
259 2 100       8 if ($conf->{schema}->subscription) {
260             # must add "websocket" route before "any" because checked in define order
261             my $subs_route_handler = _make_subs_route_handler(
262 2         12 $handler, @{$conf}{qw(subscribe_resolver keepalive)},
  2         30  
263             );
264 6         993 $r->websocket($endpoint => $subs_route_handler);
265             }
266             $r->any(\@DEFAULT_METHODS => $endpoint => $route_handler);
267             }
268              
269             1;
270              
271             =encoding utf8
272              
273             =head1 NAME
274              
275             Mojolicious::Plugin::GraphQL - a plugin for adding GraphQL route handlers
276              
277             =head1 SYNOPSIS
278              
279             my $schema = GraphQL::Schema->from_doc(<<'EOF');
280             schema {
281             query: QueryRoot
282             }
283             type QueryRoot {
284             helloWorld: String
285             }
286             EOF
287              
288             # for Mojolicious substitute "plugin" with $app->plugin(...
289             # Mojolicious::Lite (with endpoint under "/graphql")
290             plugin GraphQL => {
291             schema => $schema, root_value => { helloWorld => 'Hello, world!' }
292             };
293              
294             # OR, equivalently:
295             plugin GraphQL => {schema => $schema, handler => sub {
296             my ($c, $body, $execute, $subscribe_fn) = @_;
297             # returns JSON-able Perl data
298             $execute->(
299             $schema,
300             $body->{query},
301             { helloWorld => 'Hello, world!' }, # $root_value
302             $c->req->headers,
303             $body->{variables},
304             $body->{operationName},
305             undef, # $field_resolver
306             $subscribe_fn ? (undef, $subscribe_fn) : (), # only passed for subs
307             );
308             }};
309              
310             # OR, with bespoke user-lookup and caching:
311             plugin GraphQL => {schema => $schema, handler => sub {
312             my ($c, $body, $execute, $subscribe_fn) = @_;
313             my $user = MyStuff::User->lookup($app->request->headers->header('X-Token'));
314             die "Invalid user\n" if !$user; # turned into GraphQL { errors => [ ... ] }
315             my $cached_result = MyStuff::RequestCache->lookup($user, $body->{query});
316             return $cached_result if $cached_result;
317             MyStuff::RequestCache->cache_and_return($execute->(
318             $schema,
319             $body->{query},
320             undef, # $root_value
321             $user, # per-request info
322             $body->{variables},
323             $body->{operationName},
324             undef, # $field_resolver
325             $subscribe_fn ? (undef, $subscribe_fn) : (), # only passed for subs
326             ));
327             };
328              
329             # With GraphiQL, on /graphql
330             plugin GraphQL => {schema => $schema, graphiql => 1};
331              
332             =head1 DESCRIPTION
333              
334             This plugin allows you to easily define a route handler implementing a
335             GraphQL endpoint, including a websocket for subscriptions following
336             Apollo's C<subscriptions-transport-ws> protocol.
337              
338             As of version 0.09, it will supply the necessary C<promise_code>
339             parameter to L<GraphQL::Execution/execute>. This means your resolvers
340             can (and indeed should) return Promise objects to function
341             asynchronously. As of 0.15 these must be "Promises/A+" as subscriptions
342             require C<resolve> and C<reject> methods.
343              
344             The route handler code will be compiled to behave like the following:
345              
346             =over 4
347              
348             =item *
349              
350             Passes to the L<GraphQL> execute, possibly via your supplied handler,
351             the given schema, C<$root_value> and C<$field_resolver>. Note as above
352             that the wrapper used in this plugin will supply the hash-ref matching
353             L<GraphQL::Type::Library/PromiseCode>.
354              
355             =item *
356              
357             The action built matches POST / GET requests.
358              
359             =item *
360              
361             Returns GraphQL results in JSON form.
362              
363             =back
364              
365             =head1 OPTIONS
366              
367             L<Mojolicious::Plugin::GraphQL> supports the following options.
368              
369             =head2 convert
370              
371             Array-ref. First element is a classname-part, which will be prepended with
372             "L<GraphQL::Plugin::Convert>::". The other values will be passed
373             to that class's L<GraphQL::Plugin::Convert/to_graphql> method. The
374             returned hash-ref will be used to set options, particularly C<schema>,
375             and probably at least one of C<resolver> and C<root_value>.
376              
377             =head2 endpoint
378              
379             String. Defaults to C</graphql>.
380              
381             =head2 schema
382              
383             A L<GraphQL::Schema> object. As of 0.15, must be supplied.
384              
385             =head2 root_value
386              
387             An optional root value, passed to top-level resolvers.
388              
389             =head2 resolver
390              
391             An optional field resolver, replacing the GraphQL default.
392              
393             =head2 handler
394              
395             An optional route-handler, replacing the plugin's default - see example
396             above for possibilities.
397              
398             It must return JSON-able Perl data in the GraphQL format, which is a hash
399             with at least one of a C<data> key and/or an C<errors> key.
400              
401             If it throws an exception, that will be turned into a GraphQL-formatted
402             error.
403              
404             If being used for a subscription, it will be called with a fourth
405             parameter as shown above. It is safe to not handle this if you are
406             content with GraphQL's defaults.
407              
408             =head2 graphiql
409              
410             Boolean controlling whether requesting the endpoint with C<Accept:
411             text/html> will return the GraphiQL user interface. Defaults to false.
412              
413             # Mojolicious::Lite
414             plugin GraphQL => {schema => $schema, graphiql => 1};
415              
416             =head2 keepalive
417              
418             Defaults to 0, which means do not send. Otherwise will send a keep-alive
419             packet over websocket every specified number of seconds.
420              
421             =head1 METHODS
422              
423             L<Mojolicious::Plugin::GraphQL> inherits all methods from
424             L<Mojolicious::Plugin> and implements the following new ones.
425              
426             =head2 register
427              
428             my $route = $plugin->register(Mojolicious->new, {schema => $schema});
429              
430             Register renderer in L<Mojolicious> application.
431              
432             =head1 EXPORTS
433              
434             Exportable is the function C<promise_code>, which returns a hash-ref
435             suitable for passing as the 8th argument to L<GraphQL::Execution/execute>.
436              
437             =head1 SUBSCRIPTIONS
438              
439             To use subscriptions within your web app, just insert this JavaScript:
440              
441             <script src="//unpkg.com/subscriptions-transport-ws@0.9.16/browser/client.js"></script>
442             # ...
443             const subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(websocket_uri, {
444             reconnect: true
445             });
446             subscriptionsClient.request({
447             query: "subscription s($c: [String!]) {subscribe(channels: $c) {channel username dateTime message}}",
448             variables: { c: channel },
449             }).subscribe({
450             next(payload) {
451             var msg = payload.data.subscribe;
452             console.log(msg.username + ' said', msg.message);
453             },
454             error: console.error,
455             });
456              
457             Note the use of parameterised queries, where you only need to change
458             the C<variables> parameter. The above is adapted from the sample app,
459             L<https://github.com/graphql-perl/sample-mojolicious>.
460              
461             =head1 SEE ALSO
462              
463             L<GraphQL>
464              
465             L<GraphQL::Plugin::Convert>
466              
467             L<https://github.com/apollographql/subscriptions-transport-ws#client-browser>
468             - Apollo documentation
469              
470             =head1 AUTHOR
471              
472             Ed J
473              
474             Based heavily on L<Mojolicious::Plugin::PODRenderer>.
475              
476             =head1 COPYRIGHT AND LICENSE
477              
478             This is free software; you can redistribute it and/or modify it under
479             the same terms as the Perl 5 programming language system itself.
480              
481             =cut
482              
483             1;
484              
485             __DATA__
486             @@ graphiql.html.ep
487             <!--
488             Copied from https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js
489             Converted to use the simple template to capture the CGI args
490             Added the apollo-link-ws stuff, marked with "ADDED"
491             -->
492             <!--
493             The request to this GraphQL server provided the header "Accept: text/html"
494             and as a result has been presented GraphiQL - an in-browser IDE for
495             exploring GraphQL.
496             If you wish to receive JSON, provide the header "Accept: application/json" or
497             add "&raw" to the end of the URL within a browser.
498             -->
499             <!DOCTYPE html>
500             <html>
501             <head>
502             <meta charset="utf-8" />
503             <title>GraphiQL</title>
504             <meta name="robots" content="noindex" />
505             <style>
506             html, body {
507             height: 100%;
508             margin: 0;
509             overflow: hidden;
510             width: 100%;
511             }
512             </style>
513             <link href="//cdn.jsdelivr.net/npm/graphiql@<%= $graphiql_version %>/graphiql.css" rel="stylesheet" />
514             <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
515             <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
516             <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
517             <script src="//cdn.jsdelivr.net/npm/graphiql@<%= $graphiql_version %>/graphiql.min.js"></script>
518             <% if ($subscriptionEndpoint) { %>
519             <!-- ADDED -->
520             <script src="//unpkg.com/subscriptions-transport-ws@0.9.16/browser/client.js"></script>
521             <% } %>
522             </head>
523             <body>
524             <script type="module">
525             // Collect the URL parameters
526             var parameters = {};
527             window.location.search.substr(1).split('&').forEach(function (entry) {
528             var eq = entry.indexOf('=');
529             if (eq >= 0) {
530             parameters[decodeURIComponent(entry.slice(0, eq))] =
531             decodeURIComponent(entry.slice(eq + 1));
532             }
533             });
534             // Produce a Location query string from a parameter object.
535             function locationQuery(params) {
536             return '?' + Object.keys(params).filter(function (key) {
537             return Boolean(params[key]);
538             }).map(function (key) {
539             return encodeURIComponent(key) + '=' +
540             encodeURIComponent(params[key]);
541             }).join('&');
542             }
543             // Derive a fetch URL from the current URL, sans the GraphQL parameters.
544             var graphqlParamNames = {
545             query: true,
546             variables: true,
547             operationName: true
548             };
549             var otherParams = {};
550             for (var k in parameters) {
551             if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
552             otherParams[k] = parameters[k];
553             }
554             }
555             var fetchURL = locationQuery(otherParams);
556             // Defines a GraphQL fetcher using the fetch API.
557             function graphQLFetcher(graphQLParams) {
558             return fetch(fetchURL, {
559             method: 'post',
560             headers: {
561             'Accept': 'application/json',
562             'Content-Type': 'application/json'
563             },
564             body: JSON.stringify(graphQLParams),
565             credentials: 'include',
566             }).then(function (response) {
567             return response.text();
568             }).then(function (responseBody) {
569             try {
570             return JSON.parse(responseBody);
571             } catch (error) {
572             return responseBody;
573             }
574             });
575             }
576             // When the query and variables string is edited, update the URL bar so
577             // that it can be easily shared.
578             function onEditQuery(newQuery) {
579             parameters.query = newQuery;
580             updateURL();
581             }
582             function onEditVariables(newVariables) {
583             parameters.variables = newVariables;
584             updateURL();
585             }
586             function onEditOperationName(newOperationName) {
587             parameters.operationName = newOperationName;
588             updateURL();
589             }
590             function updateURL() {
591             history.replaceState(null, null, locationQuery(parameters));
592             }
593             // this section ADDED
594             <% if ($subscriptionEndpoint) { %>
595              
596             // this replaces the apollo GraphiQL-Subscriptions-Fetcher which is now incompatible with 0.6+ of subscriptions-transport-ws
597             // based on matiasanaya PR to fix but with improvement to only look at definition of operation being executed
598             import { parse } from "//unpkg.com/graphql@15.0.0/language/index.mjs";
599             const subsGraphQLFetcher = (subscriptionsClient, fallbackFetcher) => {
600             const hasSubscriptionOperation = (graphQlParams) => {
601             const thisOperation = graphQlParams.operationName;
602             const queryDoc = parse(graphQlParams.query);
603             const opDefinitions = queryDoc.definitions.filter(
604             x => x.kind === 'OperationDefinition'
605             );
606             const thisDefinition = opDefinitions.length == 1
607             ? opDefinitions[0]
608             : opDefinitions.filter(x => x.name.value === thisOperation)[0];
609             return thisDefinition.operation === 'subscription';
610             };
611             let activeSubscription = false;
612             return (graphQLParams) => {
613             if (subscriptionsClient && activeSubscription) {
614             subscriptionsClient.unsubscribeAll();
615             }
616             if (subscriptionsClient && hasSubscriptionOperation(graphQLParams)) {
617             activeSubscription = true;
618             return subscriptionsClient.request(graphQLParams);
619             } else {
620             return fallbackFetcher(graphQLParams);
621             }
622             };
623             };
624              
625             var subscriptionEndpoint = <%== $subscriptionEndpoint %>;
626             let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndpoint, {
627             lazy: true, // not in original
628             reconnect: true
629             });
630             let myCustomFetcher = subsGraphQLFetcher(subscriptionsClient, graphQLFetcher);
631             <% } else { %>
632             let myCustomFetcher = graphQLFetcher;
633             <% } %>
634             // end ADDED
635             // Render <GraphiQL /> into the body.
636             ReactDOM.render(
637             React.createElement(GraphiQL, {
638             fetcher: myCustomFetcher, // ADDED changed from graphQLFetcher
639             onEditQuery: onEditQuery,
640             onEditVariables: onEditVariables,
641             onEditOperationName: onEditOperationName,
642             query: <%== $queryString %>,
643             response: <%== $resultString %>,
644             variables: <%== $variablesString %>,
645             operationName: <%== $operationName %>,
646             }),
647             document.body
648             );
649             </script>
650             </body>
651             </html>