File Coverage

blib/lib/Mojolicious/Plugin/GraphQL.pm
Criterion Covered Total %
statement 153 158 96.8
branch 41 52 78.8
condition 11 15 73.3
subroutine 30 30 100.0
pod 1 2 50.0
total 236 257 91.8


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