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   73352 use strict;
  4         90  
  1         40  
4 1     1   6 use warnings;
  1         3  
  1         41  
5 1     1   6 use Mojo::Base 'Mojolicious::Plugin';
  1         2  
  1         9  
6 1     1   266 use Mojo::JSON qw(decode_json to_json);
  1         3  
  1         82  
7 1     1   608 use GraphQL::Execution qw(execute);
  1         188599  
  1         98  
8 1     1   9 use GraphQL::Type::Library -all;
  1         3  
  1         11  
9 1     1   13106 use GraphQL::Debug qw(_debug);
  1         3  
  1         76  
10 1     1   6 use Module::Runtime qw(require_module);
  1         3  
  1         13  
11 1     1   49 use Mojo::Promise;
  1         3  
  1         25  
12 1     1   1088 use curry;
  1         368  
  1         44  
13 1     1   7 use Exporter 'import';
  1         3  
  1         81  
14              
15             our $VERSION = '0.17';
16             our @EXPORT_OK = qw(promise_code);
17              
18 1     1   8 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  1         2  
  1         196  
19             use constant promise_code => +{
20             all => sub {
21             # current Mojo::Promise->all only works on promises, force that
22 1 50       6705 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         18 Mojo::Promise->all(@promises);
28             },
29 1         20 resolve => Mojo::Promise->curry::resolve,
30             reject => Mojo::Promise->curry::reject,
31             new => Mojo::Promise->curry::new,
32 1     1   8 };
  1         26  
33             # from https://github.com/apollographql/subscriptions-transport-ws/blob/master/src/message-types.ts
34 1         137 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   149 };
  1         3  
48              
49             my @DEFAULT_METHODS = qw(get post);
50 1     1   8 use constant EXECUTE => sub { $_[7] = promise_code(); goto &execute; };
  1         3  
  1         112  
  3         84  
  1         8  
51             use constant SUBSCRIBE => sub {
52 3         114 splice @_, 7, 1, promise_code();
53 4         13 goto &GraphQL::Subscription::subscribe;
54 1     1   3 };
  1         2486  
  3         117  
55             sub make_code_closure {
56 4     4 0 40 my ($schema, $root_value, $field_resolver) = @_;
57             sub {
58 5     5   29 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       251 $field_resolver,
67             $subscribe_resolver ? (undef, $subscribe_resolver) : (),
68             );
69 5         24 };
70             }
71              
72             sub _safe_serialize {
73 1   100 12   7 my $data = shift // return 'undefined';
74 1         35 my $json = to_json($data);
75 1         4 $json =~ s#/#\\/#g;
76 2         9 return $json;
77             }
78              
79             sub _graphiql_wrap {
80 2     2   11 my ($wrappee, $use_subscription) = @_;
81             sub {
82 4     4   17 my ($c) = @_;
83 3 100 100     387 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         79 my $p = $c->req->query_params;
89 1 100       41 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         167 goto $wrappee;
107 4         69028 };
108             }
109              
110             sub _decode {
111 12     12   30 my ($bytes) = @_;
112 12         49 my $body = eval { decode_json($bytes) };
  12         3069  
113             # conceal error info like versions from attackers
114 11 100       45 return (0, { errors => [ { message => "Malformed request" } ] }) if $@;
115 7         29 (1, $body);
116             }
117              
118             sub _execute {
119 7     7   19 my ($c, $body, $handler, $execute, $subscribe_fn) = @_;
120 7         34 my $data = eval { $handler->($c, $body, $execute, $subscribe_fn) };
  7         226465  
121 6 100       32 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   23 my ($c) = @_;
129 5         34 my ($decode_ok, $body) = _decode($c->req->body);
130 4 100       18 return $c->render(json => $body) if !$decode_ok;
131 4         28 my $data = _execute($c, $body, $handler, EXECUTE());
132 1 100       60 return $c->render(json => $data) if !is_Promise($data);
133 1         502512 $data->then(sub { $c->render(json => shift) });
  3         10  
134 5         33208 };
135             }
136              
137             sub _make_connection_handler {
138 3     3   56 my ($handler, $subscribe_resolver, $context) = @_;
139             sub {
140 7     7   31 my ($c, $bytes) = @_;
141 7         28 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         53 my $msg_type = $body->{type};
148 3 100       16 if ($msg_type eq ws_protocol->{GQL_CONNECTION_INIT}) {
    100          
    50          
149 3         28 $context->{connected} = 1;
150             $c->send({json => {
151             type => ws_protocol->{GQL_CONNECTION_ACK}
152 3         726 }});
153 1 100       3 if ($context->{keepalive}) {
154 1         6 my $cb;
155             $cb = sub {
156 2 50 33     60 return unless $c->tx and $context->{keepalive};
157             $c->send({json => {
158             type => ws_protocol->{GQL_CONNECTION_KEEP_ALIVE},
159 2         1205 }});
160 1         4 Mojo::IOLoop->timer($context->{keepalive} => $cb);
161 2         61210 };
162 3         92 $cb->();
163             }
164 3         13 return;
165             } elsif ($msg_type eq ws_protocol->{GQL_START}) {
166 3         21 $context->{id} = $body->{id};
167             my $data = _execute(
168 3         23 $c, $body->{payload}, $handler, SUBSCRIBE(), $subscribe_resolver,
169             );
170             return $c->send({json => {
171             payload => $data, type => ws_protocol->{GQL_ERROR},
172 3 50       87 }}) if !is_Promise($data);
173             $data->then(
174             sub {
175 3         16 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         33 return;
184             }
185 3         22 my $promise;
186             $context->{async_iterator} = $result->map_then(sub {
187 5         109 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         2312 }});
193 5         1869 $promise = $context->{async_iterator}->next_p;
194             $c->send({json => {
195             type => ws_protocol->{GQL_COMPLETE},
196             id => $context->{id},
197 3 100       215 }}) if !$promise; # exhausted, tell client
198 5         214688 });
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         11 $c->finish;
207             },
208 3         1156 );
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         285 }});
214 1 50       91 $context->{async_iterator}->close_tap if $context->{async_iterator};
215 2         10 undef %$context; # relinquish our refcounts
216             }
217             }
218 7         33175 }
219              
220             sub _make_subs_route_handler {
221 2     2   1358 my ($handler, $subscribe_resolver, $keepalive) = @_;
222 2         75141 require GraphQL::Subscription;
223             sub {
224 3     3   38 my ($c) = @_;
225             # without this, GraphiQL won't accept is valid
226 3         96 my $sec_websocket_protocol = $c->tx->req->headers->sec_websocket_protocol;
227 3 50       37 $c->tx->res->headers->sec_websocket_protocol($sec_websocket_protocol)
228             if $sec_websocket_protocol;
229 3         19 my %context = (keepalive => $keepalive);
230 3         756 $c->on(text => _make_connection_handler($handler, $subscribe_resolver, \%context));
231             $c->on(finish => sub {
232 3 100       158 $context{async_iterator}->close_tap if $context{async_iterator};
233 6         33889 undef %context; # relinquish our refcounts
234 3         17691 });
235 3         37654 };
236             }
237              
238             sub register {
239 6     6 1 39 my ($self, $app, $conf) = @_;
240 3 100       7 if ($conf->{convert}) {
241 3         13 my ($class, @values) = @{ $conf->{convert} };
  3         14  
242 3         23 $class = "GraphQL::Plugin::Convert::$class";
243 3         3470 require_module $class;
244 3         168888 my $converted = $class->to_graphql(@values);
245 6         32 $conf = { %$conf, %$converted };
246             }
247 6 50       33 die "Need schema\n" if !$conf->{schema};
248 6   100     48 my $endpoint = $conf->{endpoint} || '/graphql';
249             my $handler = $conf->{handler} || make_code_closure(
250 6   66     14 @{$conf}{qw(schema root_value resolver)}
251             );
252 6         50 push @{$app->renderer->classes}, __PACKAGE__
253 1 100       42 unless grep $_ eq __PACKAGE__, @{$app->renderer->classes};
  6         110  
254 6         41 my $route_handler = _make_route_handler($handler);
255             $route_handler = _graphiql_wrap($route_handler, $conf->{schema}->subscription)
256 6 100       28 if $conf->{graphiql};
257 6         57 my $r = $app->routes;
258 2 100       8 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         38  
262             );
263 6         1030 $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 SEE ALSO
437              
438             L<GraphQL>
439              
440             L<GraphQL::Plugin::Convert>
441              
442             =head1 AUTHOR
443              
444             Ed J
445              
446             Based heavily on L<Mojolicious::Plugin::PODRenderer>.
447              
448             =head1 COPYRIGHT AND LICENSE
449              
450             This is free software; you can redistribute it and/or modify it under
451             the same terms as the Perl 5 programming language system itself.
452              
453             =cut
454              
455             1;
456              
457             __DATA__
458             @@ graphiql.html.ep
459             <!--
460             Copied from https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js
461             Converted to use the simple template to capture the CGI args
462             Added the apollo-link-ws stuff, marked with "ADDED"
463             -->
464             <!--
465             The request to this GraphQL server provided the header "Accept: text/html"
466             and as a result has been presented GraphiQL - an in-browser IDE for
467             exploring GraphQL.
468             If you wish to receive JSON, provide the header "Accept: application/json" or
469             add "&raw" to the end of the URL within a browser.
470             -->
471             <!DOCTYPE html>
472             <html>
473             <head>
474             <meta charset="utf-8" />
475             <title>GraphiQL</title>
476             <meta name="robots" content="noindex" />
477             <style>
478             html, body {
479             height: 100%;
480             margin: 0;
481             overflow: hidden;
482             width: 100%;
483             }
484             </style>
485             <link href="//cdn.jsdelivr.net/npm/graphiql@<%= $graphiql_version %>/graphiql.css" rel="stylesheet" />
486             <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
487             <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
488             <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
489             <script src="//cdn.jsdelivr.net/npm/graphiql@<%= $graphiql_version %>/graphiql.min.js"></script>
490             <% if ($subscriptionEndpoint) { %>
491             <!-- ADDED -->
492             <script src="//unpkg.com/subscriptions-transport-ws@0.9.16/browser/client.js"></script>
493             <% } %>
494             </head>
495             <body>
496             <script type="module">
497             // Collect the URL parameters
498             var parameters = {};
499             window.location.search.substr(1).split('&').forEach(function (entry) {
500             var eq = entry.indexOf('=');
501             if (eq >= 0) {
502             parameters[decodeURIComponent(entry.slice(0, eq))] =
503             decodeURIComponent(entry.slice(eq + 1));
504             }
505             });
506             // Produce a Location query string from a parameter object.
507             function locationQuery(params) {
508             return '?' + Object.keys(params).filter(function (key) {
509             return Boolean(params[key]);
510             }).map(function (key) {
511             return encodeURIComponent(key) + '=' +
512             encodeURIComponent(params[key]);
513             }).join('&');
514             }
515             // Derive a fetch URL from the current URL, sans the GraphQL parameters.
516             var graphqlParamNames = {
517             query: true,
518             variables: true,
519             operationName: true
520             };
521             var otherParams = {};
522             for (var k in parameters) {
523             if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
524             otherParams[k] = parameters[k];
525             }
526             }
527             var fetchURL = locationQuery(otherParams);
528             // Defines a GraphQL fetcher using the fetch API.
529             function graphQLFetcher(graphQLParams) {
530             return fetch(fetchURL, {
531             method: 'post',
532             headers: {
533             'Accept': 'application/json',
534             'Content-Type': 'application/json'
535             },
536             body: JSON.stringify(graphQLParams),
537             credentials: 'include',
538             }).then(function (response) {
539             return response.text();
540             }).then(function (responseBody) {
541             try {
542             return JSON.parse(responseBody);
543             } catch (error) {
544             return responseBody;
545             }
546             });
547             }
548             // When the query and variables string is edited, update the URL bar so
549             // that it can be easily shared.
550             function onEditQuery(newQuery) {
551             parameters.query = newQuery;
552             updateURL();
553             }
554             function onEditVariables(newVariables) {
555             parameters.variables = newVariables;
556             updateURL();
557             }
558             function onEditOperationName(newOperationName) {
559             parameters.operationName = newOperationName;
560             updateURL();
561             }
562             function updateURL() {
563             history.replaceState(null, null, locationQuery(parameters));
564             }
565             // this section ADDED
566             <% if ($subscriptionEndpoint) { %>
567              
568             // this replaces the apollo GraphiQL-Subscriptions-Fetcher which is now incompatible with 0.6+ of subscriptions-transport-ws
569             // based on matiasanaya PR to fix but with improvement to only look at definition of operation being executed
570             import { parse } from "//unpkg.com/graphql@15.0.0/language/index.mjs";
571             const subsGraphQLFetcher = (subscriptionsClient, fallbackFetcher) => {
572             const hasSubscriptionOperation = (graphQlParams) => {
573             const thisOperation = graphQlParams.operationName;
574             const queryDoc = parse(graphQlParams.query);
575             const opDefinitions = queryDoc.definitions.filter(
576             x => x.kind === 'OperationDefinition'
577             );
578             const thisDefinition = opDefinitions.length == 1
579             ? opDefinitions[0]
580             : opDefinitions.filter(x => x.name.value === thisOperation)[0];
581             return thisDefinition.operation === 'subscription';
582             };
583             let activeSubscription = false;
584             return (graphQLParams) => {
585             if (subscriptionsClient && activeSubscription) {
586             subscriptionsClient.unsubscribeAll();
587             }
588             if (subscriptionsClient && hasSubscriptionOperation(graphQLParams)) {
589             activeSubscription = true;
590             return subscriptionsClient.request(graphQLParams);
591             } else {
592             return fallbackFetcher(graphQLParams);
593             }
594             };
595             };
596              
597             var subscriptionEndpoint = <%== $subscriptionEndpoint %>;
598             let subscriptionsClient = new window.SubscriptionsTransportWs.SubscriptionClient(subscriptionEndpoint, {
599             lazy: true, // not in original
600             reconnect: true
601             });
602             let myCustomFetcher = subsGraphQLFetcher(subscriptionsClient, graphQLFetcher);
603             <% } else { %>
604             let myCustomFetcher = graphQLFetcher;
605             <% } %>
606             // end ADDED
607             // Render <GraphiQL /> into the body.
608             ReactDOM.render(
609             React.createElement(GraphiQL, {
610             fetcher: myCustomFetcher, // ADDED changed from graphQLFetcher
611             onEditQuery: onEditQuery,
612             onEditVariables: onEditVariables,
613             onEditOperationName: onEditOperationName,
614             query: <%== $queryString %>,
615             response: <%== $resultString %>,
616             variables: <%== $variablesString %>,
617             operationName: <%== $operationName %>,
618             }),
619             document.body
620             );
621             </script>
622             </body>
623             </html>