File Coverage

blib/lib/Dancer2/Plugin/GraphQL.pm
Criterion Covered Total %
statement 23 26 88.4
branch 1 2 50.0
condition n/a
subroutine 9 9 100.0
pod 0 1 0.0
total 33 38 86.8


line stmt bran cond sub pod time code
1             # ABSTRACT: a plugin for adding GraphQL route handlers
2             use strict;
3 1     1   773180 use warnings;
  1         7  
  1         24  
4 1     1   6 use Dancer2::Core::Types qw(Bool);
  1         1  
  1         22  
5 1     1   5 use Dancer2::Plugin;
  1         2  
  1         5  
6 1     1   1268 use GraphQL::Execution qw(execute);
  1         10624  
  1         6  
7 1     1   3192 use Module::Runtime qw(require_module);
  1         1440295  
  1         110  
8 1     1   9  
  1         2  
  1         6  
9             our $VERSION = '0.09';
10              
11             has graphiql => (
12             is => 'ro',
13             isa => Bool,
14             from_config => sub { '' },
15             );
16              
17             my @DEFAULT_METHODS = qw(get post);
18             my $TEMPLATE = join '', <DATA>;
19             my $EXECUTE = sub {
20             my ($schema, $query, $root_value, $per_request, $variables, $operationName, $field_resolver) = @_;
21             execute(
22             $schema,
23             $query,
24             $root_value,
25             $per_request,
26             $variables,
27             $operationName,
28             $field_resolver,
29             );
30             };
31             my ($schema, $root_value, $field_resolver) = @_;
32             sub {
33 1     1 0 11 my ($app, $body, $execute) = @_;
34             $execute->(
35 1     1   4 $schema,
36             $body->{query},
37             $root_value,
38             $app->request->headers,
39             $body->{variables},
40             $body->{operationName},
41             $field_resolver,
42             );
43 1         5 };
44             };
45 1         6  
46             my $JSON = JSON::MaybeXS->new->utf8->allow_nonref;
47             my $data = shift or return 'undefined';
48             my $json = $JSON->encode( $data );
49             $json =~ s#/#\\/#g;
50 4 50   4   64 return $json;
51 0           }
52 0            
53 0           # DSL args after $pattern: $schema, $root_value, $resolver, $handler
54             plugin_keywords graphql => sub {
55             my ($plugin, $pattern, @rest) = @_;
56             my ($schema, $root_value, $field_resolver, $handler);
57             if (ref $rest[0] eq 'ARRAY') {
58             my ($class, @values) = @{ shift @rest };
59             $class = "GraphQL::Plugin::Convert::$class";
60             require_module $class;
61             my $converted = $class->to_graphql(@values);
62             unshift @rest, @{$converted}{qw(schema root_value resolver)};
63             push @rest, make_code_closure(@rest[0..2]) if @rest < 4;
64             }
65             if (@rest == 4) {
66             ($schema, $root_value, $field_resolver, $handler) = @rest;
67             } else {
68             ($schema, $root_value) = grep ref ne 'CODE', @rest;
69             my @codes = grep ref eq 'CODE', @rest;
70             # if only one, is $handler
71             ($handler, $field_resolver) = reverse @codes;
72             $handler ||= make_code_closure($schema, $root_value, $field_resolver);
73             }
74             my $ajax_route = sub {
75             my ($app) = @_;
76             if (
77             $plugin->graphiql and
78             ($app->request->header('Accept')//'') =~ /^text\/html\b/ and
79             !defined $app->request->params->{raw}
80             ) {
81             # disable layout
82             my $layout = $app->config->{layout};
83             $app->config->{layout} = undef;
84             my $result = $app->template(\$TEMPLATE, {
85             title => 'GraphiQL',
86             graphiql_version => 'latest',
87             queryString => _safe_serialize( $app->request->params->{query} ),
88             operationName => _safe_serialize( $app->request->params->{operationName} ),
89             resultString => _safe_serialize( $app->request->params->{result} ),
90             variablesString => _safe_serialize( $app->request->params->{variables} ),
91             });
92             $app->config->{layout} = $layout;
93             $app->send_as(html => $result);
94             }
95             my $body = $JSON->decode($app->request->body);
96             my $data = eval { $handler->($app, $body, $EXECUTE) };
97             $data = { errors => [ { message => $@ } ] } if $@;
98             $app->send_as(JSON => $data);
99             };
100             foreach my $method (@DEFAULT_METHODS) {
101             $plugin->app->add_route(
102             method => $method,
103             regexp => $pattern,
104             code => $ajax_route,
105             );
106             }
107             };
108              
109             =pod
110              
111             =encoding UTF-8
112              
113             =head1 NAME
114              
115             Dancer2::Plugin::GraphQL - a plugin for adding GraphQL route handlers
116              
117             =head1 SYNOPSIS
118              
119             package MyWebApp;
120              
121             use Dancer2;
122             use Dancer2::Plugin::GraphQL;
123             use GraphQL::Schema;
124              
125             my $schema = GraphQL::Schema->from_doc(<<'EOF');
126             schema {
127             query: QueryRoot
128             }
129             type QueryRoot {
130             helloWorld: String
131             }
132             EOF
133             graphql '/graphql' => $schema, { helloWorld => 'Hello, world!' };
134              
135             dance;
136              
137             # OR, equivalently:
138             graphql '/graphql' => $schema => sub {
139             my ($app, $body, $execute) = @_;
140             # returns JSON-able Perl data
141             $execute->(
142             $schema,
143             $body->{query},
144             undef, # $root_value
145             $app->request->headers,
146             $body->{variables},
147             $body->{operationName},
148             undef, # $field_resolver
149             );
150             };
151              
152             # OR, with bespoke user-lookup and caching:
153             graphql '/graphql' => sub {
154             my ($app, $body, $execute) = @_;
155             my $user = MyStuff::User->lookup($app->request->headers->header('X-Token'));
156             die "Invalid user\n" if !$user; # turned into GraphQL { errors => [ ... ] }
157             my $cached_result = MyStuff::RequestCache->lookup($user, $body->{query});
158             return $cached_result if $cached_result;
159             MyStuff::RequestCache->cache_and_return($execute->(
160             $schema,
161             $body->{query},
162             undef, # $root_value
163             $user, # per-request info
164             $body->{variables},
165             $body->{operationName},
166             undef, # $field_resolver
167             ));
168             };
169              
170             =head1 DESCRIPTION
171              
172             The C<graphql> keyword which is exported by this plugin allow you to
173             define a route handler implementing a GraphQL endpoint.
174              
175             Parameters, after the route pattern.
176             The first three can be replaced with a single array-ref. If so,
177             the first element is a classname-part, which will be prepended with
178             "L<GraphQL::Plugin::Convert>::". The other values will be passed to
179             that class's L<GraphQL::Plugin::Convert/to_graphql> method. The returned
180             hash-ref will be used to set options.
181              
182             E.g.
183              
184             graphql '/graphql' => [ 'Test' ]; # uses GraphQL::Plugin::Convert::Test
185              
186             =over 4
187              
188             =item $schema
189              
190             A L<GraphQL::Schema> object.
191              
192             =item $root_value
193              
194             An optional root value, passed to top-level resolvers.
195              
196             =item $field_resolver
197              
198             An optional field resolver, replacing the GraphQL default.
199              
200             =item $route_handler
201              
202             An optional route-handler, replacing the plugin's default - see example
203             above for possibilities.
204              
205             It must return JSON-able Perl data in the GraphQL format, which is a hash
206             with at least one of a C<data> key and/or an C<errors> key.
207              
208             If it throws an exception, that will be turned into a GraphQL-formatted
209             error.
210              
211             =back
212              
213             If you supply two code-refs, they will be the C<$resolver> and
214             C<$handler>. If you only supply one, it will be C<$handler>. To be
215             certain, pass all four post-pattern arguments.
216              
217             The route handler code will be compiled to behave like the following:
218              
219             =over 4
220              
221             =item *
222              
223             Passes to the L<GraphQL> execute, possibly via your supplied handler,
224             the given schema, C<$root_value> and C<$field_resolver>.
225              
226             =item *
227              
228             The action built matches POST / GET requests.
229              
230             =item *
231              
232             Returns GraphQL results in JSON form.
233              
234             =back
235              
236             =head1 CONFIGURATION
237              
238             By default the plugin will not return GraphiQL, but this can be overridden
239             with plugin setting 'graphiql', to true.
240              
241             Here is example to use GraphiQL:
242              
243             plugins:
244             GraphQL:
245             graphiql: true
246              
247             =head1 AUTHOR
248              
249             Ed J
250              
251             Based heavily on L<Dancer2::Plugin::Ajax> by "Dancer Core Developers".
252              
253             =head1 COPYRIGHT AND LICENSE
254              
255             This is free software; you can redistribute it and/or modify it under
256             the same terms as the Perl 5 programming language system itself.
257              
258             =cut
259              
260             1;
261              
262             <!--
263             Copied from https://github.com/graphql/express-graphql/blob/master/src/renderGraphiQL.js
264             Converted to use the simple template to capture the CGI args
265             Added the apollo-link-ws stuff, marked with "ADDED"
266             -->
267             <!--
268             The request to this GraphQL server provided the header "Accept: text/html"
269             and as a result has been presented GraphiQL - an in-browser IDE for
270             exploring GraphQL.
271             If you wish to receive JSON, provide the header "Accept: application/json" or
272             add "&raw" to the end of the URL within a browser.
273             -->
274             <!DOCTYPE html>
275             <html>
276             <head>
277             <meta charset="utf-8" />
278             <title>GraphiQL</title>
279             <meta name="robots" content="noindex" />
280             <style>
281             html, body {
282             height: 100%;
283             margin: 0;
284             overflow: hidden;
285             width: 100%;
286             }
287             </style>
288             <link href="//cdn.jsdelivr.net/npm/graphiql@[% graphiql_version %]/graphiql.css" rel="stylesheet" />
289             <script src="//cdn.jsdelivr.net/fetch/0.9.0/fetch.min.js"></script>
290             <script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
291             <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
292             <script src="//cdn.jsdelivr.net/npm/graphiql@[% graphiql_version %]/graphiql.min.js"></script>
293             </head>
294             <body>
295             <script type="module">
296             var defaultQuery = `
297             # Welcome to GraphiQL
298             #
299             # GraphiQL is an in-browser tool for writing, validating, and
300             # testing GraphQL queries.
301             #
302             # Type queries into this side of the screen, and you will see intelligent
303             # typeaheads aware of the current GraphQL type schema and live syntax and
304             # validation errors highlighted within the text.
305             #
306             # GraphQL queries typically start with a "{" character. Lines that start
307             # with a # are ignored.
308             #
309             # An example GraphQL query might look like:
310             #
311             # {
312             # field(arg: "value") {
313             # subField
314             # }
315             # }
316             #
317             # Calder notes:
318             #
319             # Open the "Request Headers" panel located at the bottom of this page
320             # to specify Business-Unit and other headers as a JSON object.
321             #
322             # Keyboard shortcuts:
323             #
324             # Prettify Query: Shift-Ctrl-P (or press the prettify button above)
325             #
326             # Merge Query: Shift-Ctrl-M (or press the merge button above)
327             #
328             # Run Query: Ctrl-Enter (or press the play button above)
329             #
330             # Auto Complete: Ctrl-Space (or just start typing)
331             #
332             `;
333             // Collect the URL parameters
334             var parameters = {};
335             window.location.search.substr(1).split('&').forEach(function (entry) {
336             var eq = entry.indexOf('=');
337             if (eq >= 0) {
338             parameters[decodeURIComponent(entry.slice(0, eq))] =
339             decodeURIComponent(entry.slice(eq + 1));
340             }
341             });
342             // Produce a Location query string from a parameter object.
343             function locationQuery(params) {
344             return '?' + Object.keys(params).filter(function (key) {
345             return Boolean(params[key]);
346             }).map(function (key) {
347             return encodeURIComponent(key) + '=' +
348             encodeURIComponent(params[key]);
349             }).join('&');
350             }
351             // Derive a fetch URL from the current URL, sans the GraphQL parameters.
352             var graphqlParamNames = {
353             query: true,
354             variables: true,
355             operationName: true,
356             headers: true
357             };
358             var otherParams = {};
359             for (var k in parameters) {
360             if (parameters.hasOwnProperty(k) && graphqlParamNames[k] !== true) {
361             otherParams[k] = parameters[k];
362             }
363             }
364             var fetchURL = locationQuery(otherParams);
365             // Defines a GraphQL fetcher using the fetch API.
366             function graphQLFetcher(graphQLParams) {
367             var headers = {
368             'Accept': 'application/json',
369             'Content-Type': 'application/json'
370             };
371             var extra_headers = JSON.parse(
372             parameters.headers || localStorage.getItem('graphiql:headers') || '{ }'
373             );
374             Object.assign(headers, extra_headers);
375             return fetch(fetchURL, {
376             method: 'post',
377             headers: headers,
378             body: JSON.stringify(graphQLParams),
379             credentials: 'include',
380             }).then(function (response) {
381             return response.text();
382             }).then(function (responseBody) {
383             try {
384             return JSON.parse(responseBody);
385             } catch (error) {
386             return responseBody;
387             }
388             });
389             }
390             // When the query and variables string is edited, update the URL bar so
391             // that it can be easily shared.
392             function onEditQuery(newQuery) {
393             parameters.query = newQuery;
394             updateURL();
395             }
396             function onEditVariables(newVariables) {
397             parameters.variables = newVariables;
398             updateURL();
399             }
400             function onEditOperationName(newOperationName) {
401             parameters.operationName = newOperationName;
402             updateURL();
403             }
404             function onEditHeaders(newHeaders) {
405             parameters.headers = newHeaders;
406             updateURL();
407             }
408             function updateURL() {
409             history.replaceState(null, null, locationQuery(parameters));
410             }
411             let myCustomFetcher = graphQLFetcher;
412             // Render <GraphiQL /> into the body.
413             ReactDOM.render(
414             React.createElement(GraphiQL, {
415             fetcher: myCustomFetcher, // ADDED changed from graphQLFetcher
416             onEditQuery: onEditQuery,
417             onEditVariables: onEditVariables,
418             onEditOperationName: onEditOperationName,
419             onEditHeaders: onEditHeaders,
420             defaultSecondaryEditorOpen: true,
421             shouldPersistHeaders: true,
422             defaultQuery: defaultQuery,
423             query: [% queryString %],
424             response: [% resultString %],
425             variables: [% variablesString %],
426             operationName: [% operationName %],
427             }),
428             document.body
429             );
430             </script>
431             </body>
432             </html>