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