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