File Coverage

blib/lib/GraphQL/Plugin/Convert/DBIC.pm
Criterion Covered Total %
statement 204 205 99.5
branch 76 92 82.6
condition 30 38 78.9
subroutine 26 26 100.0
pod 1 2 50.0
total 337 363 92.8


line stmt bran cond sub pod time code
1             package GraphQL::Plugin::Convert::DBIC;
2 5     5   4504 use strict;
  5         11  
  5         134  
3 5     5   25 use warnings;
  5         9  
  5         114  
4 5     5   2567 use GraphQL::Schema;
  5         7974028  
  5         299  
5 5     5   48 use GraphQL::Debug qw(_debug);
  5         12  
  5         240  
6 5     5   3110 use Lingua::EN::Inflect::Number qw(to_S to_PL);
  5         141082  
  5         43  
7 5     5   1252 use Carp qw(confess);
  5         12  
  5         351  
8              
9             our $VERSION = "0.17";
10 5     5   36 use constant DEBUG => $ENV{GRAPHQL_DEBUG};
  5         10  
  5         8764  
11              
12             my %GRAPHQL_TYPE2SQLS = (
13             String => [
14             'wlongvarchar',
15             'guid',
16             'uuid',
17             'wvarchar',
18             'wchar',
19             'longvarbinary',
20             'varbinary',
21             'binary',
22             'longvarchar',
23             'unknown_type',
24             'all_types',
25             'char',
26             'varchar',
27             'udt',
28             'udt_locator',
29             'row',
30             'ref',
31             'blob',
32             'blob_locator',
33             'clob',
34             'clob_locator',
35             'array',
36             'array_locator',
37             'multiset',
38             'multiset_locator',
39             # mysql
40             'set',
41             'text',
42             'tinytext',
43             'mediumtext',
44             'longtext',
45             # pgsql
46             'cidr',
47             'inet',
48             ],
49             Int => [
50             'bigint',
51             'bit',
52             'tinyint',
53             'integer',
54             'smallint',
55             'mediumint',
56             'interval',
57             'interval_year',
58             'interval_month',
59             'interval_day',
60             'interval_hour',
61             'interval_minute',
62             'interval_second',
63             'interval_year_to_month',
64             'interval_day_to_hour',
65             'interval_day_to_minute',
66             'interval_day_to_second',
67             'interval_hour_to_minute',
68             'interval_hour_to_second',
69             'interval_minute_to_second',
70             # not DBI SQL_* types
71             'int',
72             ],
73             Float => [
74             'numeric',
75             'decimal',
76             'float',
77             'real',
78             'double',
79             ],
80             DateTime => [
81             'datetime',
82             'date',
83             'time',
84             'timestamp',
85             'type_date',
86             'type_time',
87             'type_timestamp',
88             'type_time_with_timezone',
89             'type_timestamp_with_timezone',
90             # pgsql
91             'timestamp with time zone',
92             'timestamp without time zone',
93             ],
94             Boolean => [
95             'boolean',
96             ],
97             ID => [
98             'wvarchar',
99             ],
100             );
101             my %TYPEMAP = (
102             (map {
103             my $gql_type = $_;
104             map {
105             ($_ => $gql_type)
106             } @{ $GRAPHQL_TYPE2SQLS{$gql_type} }
107             } keys %GRAPHQL_TYPE2SQLS),
108             enum => sub {
109             my ($source, $column, $info) = @_;
110             my $extra = $info->{extra};
111             return {
112             kind => 'enum',
113             name => _dbicsource2pretty(
114             $extra->{custom_type_name} || "${source}_$column"
115             ),
116             values => { map { _trim_name($_) => { value => $_ } } @{ $extra->{list} } },
117             }
118             },
119             );
120             my %TYPE2SCALAR = map { ($_ => 1) } qw(ID String Int Float Boolean);
121              
122             sub _dbicsource2pretty {
123 83     83   156 my ($source) = @_;
124 83 50       183 confess "_dbicsource2pretty given undef" if !defined $source;
125 83   66     156 $source = eval { $source->source_name } || $source;
126 83         5719 $source =~ s#.*::##;
127 83         247 $source = to_S $source;
128 83         68086 join '', map ucfirst, split /_+/, $source;
129             }
130              
131             sub _trim_name {
132 38     38   67 my ($name) = @_;
133 38 50       81 return if !defined $name;
134 38         96 $name =~ s#[^a-zA-Z0-9_]+#_#g;
135 38         169 $name;
136             }
137              
138             sub _apply_modifier {
139 854     854   1368 my ($modifier, $typespec) = @_;
140 854 100       1466 return $typespec if !$modifier;
141 741 100 100     2138 return $typespec if $modifier eq 'non_null'
      100        
142             and ref $typespec eq 'ARRAY'
143             and $typespec->[0] eq 'non_null'; # no double-non_null
144 678         1992 [ $modifier, { type => $typespec } ];
145             }
146              
147             sub _remove_modifiers {
148 68     68   109 my ($typespec) = @_;
149 68 100       135 return _remove_modifiers($typespec->{type}) if ref $typespec eq 'HASH';
150 50 100       170 return $typespec if ref $typespec ne 'ARRAY';
151 18         40 _remove_modifiers($typespec->[1]);
152             }
153              
154             sub _type2createinput {
155 28     28   106 my ($name, $fields, $pk21, $fk21, $column21, $name2type) = @_;
156             +{
157             kind => 'input',
158             name => "${name}CreateInput",
159             fields => {
160 162         283 (map { ($_ => $fields->{$_}) }
161 28   100     328 grep !$pk21->{$_} && !$fk21->{$_}, keys %$column21),
162             _make_fk_fields($name, $fk21, $name2type),
163             },
164             };
165             }
166              
167             sub _type2idinput {
168 28     28   59 my ($name, $fields, $pk21) = @_;
169             +{
170             kind => 'input',
171             name => "${name}IDInput",
172             fields => {
173 28         84 (map { ($_ => $fields->{$_}) }
  27         116  
174             keys %$pk21),
175             },
176             };
177             }
178              
179             sub _type2searchinput {
180 30     30   67 my ($name, $column2rawtype, $pk21, $column21) = @_;
181             +{
182             kind => 'input',
183             name => "${name}SearchInput",
184             fields => {
185 171         485 (map { ($_ => { type => $column2rawtype->{$_} }) }
186 30         118 grep !$pk21->{$_}, keys %$column21),
187             },
188             };
189             }
190              
191             sub _type2updateinput {
192 28     28   48 my ($name) = @_;
193             +{
194 28         81 kind => 'input',
195             name => "${name}UpdateInput",
196             fields => {
197             id => { type => _apply_modifier('non_null', "${name}IDInput") },
198             payload => { type => _apply_modifier('non_null', "${name}SearchInput") },
199             },
200             };
201             }
202              
203             sub _make_fk_fields {
204 28     28   58 my ($name, $fk21, $name2type) = @_;
205 28         47 my $type = $name2type->{$name};
206             (map {
207 28         127 my $field_type = $type->{fields}{$_}{type};
  17         38  
208 17 100       41 if (!$TYPE2SCALAR{_remove_modifiers($field_type)}) {
209 15   66     50 my $non_null =
210             ref($field_type) eq 'ARRAY' && $field_type->[0] eq 'non_null';
211 15   100     43 $field_type = _apply_modifier(
212             $non_null && 'non_null', _remove_modifiers($field_type)."IDInput"
213             );
214             }
215 17         144 ($_ => { type => $field_type })
216             } keys %$fk21);
217             }
218              
219             sub field_resolver {
220 64     64 1 1023259 my ($root_value, $args, $context, $info) = @_;
221 64         156 my $field_name = $info->{field_name};
222 64         89 DEBUG and _debug('DBIC.resolver', $field_name, $args, $info);
223 64         201 my $parent_name = $info->{parent_type}->name;
224 64 100       233 if ($parent_name eq 'Mutation') {
    100          
225 3         16 goto &_mutation_resolver;
226             } elsif ($parent_name eq 'Query') {
227 8         48 goto &_query_resolver;
228             }
229             my $property = ref($root_value) eq 'HASH'
230 53 100       149 ? $root_value->{$field_name}
231             : $root_value;
232 53 50       105 return $property->($args, $context, $info) if ref $property eq 'CODE';
233 53 100 50     274 return $property // die "DBIC.resolver could not resolve '$field_name'\n"
      66        
234             if ref $root_value eq 'HASH' or !$root_value->can($field_name);
235 8 50       29 return $root_value->$field_name($args, $context, $info)
236             if !UNIVERSAL::isa($root_value, 'DBIx::Class::Core');
237             # dbic search
238 8         164 my $rs = $root_value->$field_name;
239 8 50       5910 $rs = [ $rs->all ] if $info->{return_type}->isa('GraphQL::Type::List');
240 8         26 return $rs;
241             }
242              
243             sub _subfieldrels {
244 13     13   41 my ($field_node) = @_;
245 13 50       45 die "_subfieldrels called on non-field" if $field_node->{kind} ne 'field';
246 13 50       20 return {} unless my @sels = @{ $field_node->{selections} || [] };
  13 50       67  
247 13 100       29 return {} unless my @withsels = grep @{ $_->{selections} || [] }, @sels;
  27 100       134  
248 4         7 +{ map { $_->{name} => _subfieldrels($_) } @withsels };
  5         18  
249             }
250              
251             sub _query_resolver {
252 8     8   23 my ($dbic_schema, $args, $context, $info) = @_;
253 8         152 my $name = $info->{return_type}->name;
254 8 100       228 my $method = $info->{return_type}->isa('GraphQL::Type::List')
255             ? 'search' : 'find';
256 8         15 my @subfieldrels = map _subfieldrels($_), @{$info->{field_nodes}};
  8         43  
257 8 100       31 $args = $args->{input} if ref $args->{input} eq 'HASH';
258 8         26 $args = +{ map { ("me.$_" => $args->{$_}) } keys %$args };
  8         115  
259 8         18 DEBUG and _debug('DBIC.root_value', $name, $method, $args, \@subfieldrels, $info);
260 8         65 my $rs = $dbic_schema->resultset($name);
261 8         4752 my $result = $rs->$method(
262             $args,
263             {
264             prefetch => { map %$_, @subfieldrels },
265             result_class => 'DBIx::Class::ResultClass::HashRefInflator'
266             },
267             );
268 8 100       8170 $result = [ $result->all ] if $method eq 'search';
269 8         115473 $result;
270             }
271              
272             sub _make_query_pk_field {
273 52     52   111 my ($typename, $type, $name2pk21, $is_list) = @_;
274 52         74 my $return_type = $typename;
275 52 100       145 $return_type = _apply_modifier('list', $return_type) if $is_list;
276             +{
277             type => $return_type,
278             args => {
279             map {
280 54         110 my $field_type = _apply_modifier('non_null', $type->{fields}{$_}{type});
281 54 100       138 $field_type = _apply_modifier('non_null', _apply_modifier('list',
282             $field_type
283             )) if $is_list;
284 54         283 $_ => { type => $field_type }
285 52         113 } keys %{ $name2pk21->{$typename} }
  52         109  
286             },
287             };
288             }
289              
290             sub _make_input_field {
291 114     114   214 my ($typename, $return_type, $mutation_kind, $list_in, $list_out) = @_;
292 114 50       243 $return_type = _apply_modifier('list', $return_type) if $list_out;
293 114         263 my $input_type = $typename . ucfirst($mutation_kind) . 'Input';
294 114         199 $input_type = _apply_modifier('non_null', $input_type);
295 114 100       234 $input_type = _apply_modifier('non_null', _apply_modifier('list',
296             $input_type
297             )) if $list_in;
298             +{
299 114         670 type => $return_type,
300             args => { input => { type => $input_type } },
301             };
302             }
303              
304             use constant MUTATE_IDPROCESS => {
305 3         30 update => sub { $_[0]->{id} },
306 1         6 delete => sub { $_[0] },
307 5     5   40 };
  5         27  
  5         533  
308             use constant MUTATE_ARGSPROCESS => {
309 2         19 update => sub { $_[0]->{payload} },
310             delete => sub { },
311 5     5   37 };
  5         12  
  5         672  
312             use constant MUTATE_POSTPROCESS => {
313 2 50       44 update => sub { ref($_[0]) eq 'GraphQL::Error' ? $_[0] : $_[0]->discard_changes },
314 1 50 50     15 delete => sub { ref($_[0]) eq 'GraphQL::Error' ? $_[0] : $_[0] && 1 },
315 5     5   35 };
  5         48  
  5         6560  
316             sub _mutation_resolver {
317 3     3   10 my ($dbic_schema, $args, $context, $info) = @_;
318 3         8 my $name = $info->{field_name};
319 3 50       26 die "Couldn't understand field '$name'"
320             unless $name =~ s/^(create|update|delete)//;
321 3         11 my $method = $1;
322 3         6 my $find_first = $method ne 'create';
323 3         21 my ($id_process, $args_process, $result_process) = map $_->{$method},
324             MUTATE_IDPROCESS, MUTATE_ARGSPROCESS, MUTATE_POSTPROCESS;
325 3 50       14 $args = $args->{input} if $args->{input};
326 3         8 my $is_list = ref $args eq 'ARRAY';
327 3 50       10 $args = [ $args ] if !$is_list; # so can just deal as list below
328 3         6 DEBUG and _debug("DBIC._mutation_resolver", $info->{field_name}, [ $id_process ? map $id_process->($_), @$args : () ], $args);
329 3         21 my $rs = $dbic_schema->resultset($name);
330             my $all_result = [
331             map {
332 3         1621 my $operand = $rs;
  5         12  
333 5 100       24 $operand = $operand->find($id_process->($_)) if $find_first;
334 5 100       14076 my $result = $operand
    100          
335             ? $operand->$method($args_process ? $args_process->($_) : $_)
336             : GraphQL::Error->coerce("$name not found");
337 5 100 100     67335 $result = $result_process->($result)
338             if $result_process and ref($result) ne 'GraphQL::Error';
339 5         10104 $result;
340             } @$args
341             ];
342 3 50       85 $all_result = $all_result->[0] if !$is_list;
343 3         18 $all_result
344             }
345              
346             sub to_graphql {
347 6     6 0 223520 my ($class, $dbic_schema) = @_;
348 6 50 50     46 $dbic_schema = $dbic_schema->() if ((ref($dbic_schema)||'') eq 'CODE');
349 6         15 my @ast;
350             my (
351 6         16 %name2type, %name2column21, %name2pk21, %name2fk21,
352             %name2column2rawtype, %seentype, %name2isview,
353             );
354 6         34 for my $source (map $dbic_schema->source($_), $dbic_schema->sources) {
355 30         1232 my $name = _dbicsource2pretty($source);
356 30         60 DEBUG and _debug("schema_dbic2graphql($name)", $source);
357 30 100       224 $name2isview{$name} = 1 if $source->can('view_definition');
358 30         51 my %fields;
359 30         130 my $columns_info = $source->columns_info;
360 30         1097 $name2pk21{$name} = +{ map { ($_ => 1) } $source->primary_columns };
  33         392  
361             my %rel2info = map {
362 30         331 ($_ => $source->relationship_info($_))
  40         267  
363             } $source->relationships;
364 30         378 for my $column (keys %$columns_info) {
365 216         315 my $info = $columns_info->{$column};
366 216         256 DEBUG and _debug("schema_dbic2graphql($name.col)", $column, $info);
367 216         435 my $rawtype = $TYPEMAP{ lc $info->{data_type} };
368 216 100       406 if ( 'CODE' eq ref $rawtype ) {
369 13         29 my $col_spec = $rawtype->($name, $column, $info);
370 13 100       56 push @ast, $col_spec unless $seentype{$col_spec->{name}};
371 13         28 $rawtype = $col_spec->{name};
372 13         38 $seentype{$col_spec->{name}} = 1;
373             }
374 216         386 $name2column2rawtype{$name}->{$column} = $rawtype;
375             my $fulltype = _apply_modifier(
376 216   100     697 !$info->{is_nullable} && 'non_null',
      50        
377             $rawtype
378 0         0 // die "'$column' unknown data type: @{[lc $info->{data_type}]}\n",
379             );
380 216         475 $fields{$column} = +{ type => $fulltype };
381 216 100       422 $name2fk21{$name}->{$column} = 1 if $info->{is_foreign_key};
382 216         451 $name2column21{$name}->{$column} = 1;
383             }
384 30         79 for my $rel (keys %rel2info) {
385 40         68 my $info = $rel2info{$rel};
386 40         56 DEBUG and _debug("schema_dbic2graphql($name.rel)", $rel, $info);
387 40         98 my $type = _dbicsource2pretty($info->{source});
388 40         97 $rel =~ s/_id$//; # dumb heuristic
389 40         89 delete $name2column21{$name}->{$rel}; # so it's not a "column" now
390 40         72 delete $name2pk21{$name}{$rel}; # it's not a PK either
391             # if it WAS a column, capture its non-null-ness
392 40   100     180 my $non_null = ref(($fields{$rel} || {})->{type}) eq 'ARRAY';
393 40 100       96 $type = _apply_modifier('non_null', $type) if $non_null;
394 40 100       150 $type = _apply_modifier('list', $type) if $info->{attrs}{accessor} eq 'multi';
395 40 100       88 $type = _apply_modifier('non_null', $type) if $non_null; # in case list
396 40         146 $fields{$rel} = +{ type => $type };
397             }
398 30         116 my $spec = +{
399             kind => 'type',
400             name => $name,
401             fields => \%fields,
402             };
403 30         62 $name2type{$name} = $spec;
404 30         128 push @ast, $spec;
405             }
406             push @ast, map _type2idinput(
407             $_, $name2type{$_}->{fields}, $name2pk21{$_},
408             $name2column21{$_},
409 6   66     74 ), grep !$name2isview{$_} || keys %{ $name2pk21{$_} }, keys %name2type;
410             push @ast, map _type2createinput(
411             $_, $name2type{$_}->{fields}, $name2pk21{$_}, $name2fk21{$_},
412             $name2column21{$_}, \%name2type,
413 6         51 ), grep !$name2isview{$_}, keys %name2type;
414             push @ast, map _type2searchinput(
415             $_, $name2column2rawtype{$_}, $name2pk21{$_},
416 6         35 $name2column21{$_},
417             ), keys %name2type;
418 6         40 push @ast, map _type2updateinput($_), grep !$name2isview{$_}, keys %name2type;
419             push @ast, {
420             kind => 'type',
421             name => 'Query',
422             fields => {
423             map {
424 6         24 my $name = $_;
  30         53  
425 30         51 my $type = $name2type{$name};
426 30         68 my $pksearch_name = lcfirst $name;
427 30         93 my $pksearch_name_plural = to_PL($pksearch_name);
428 30         25374 my $input_search_name = "search$name";
429 30         85 my @fields = (
430             $input_search_name => _make_input_field($name, $name, 'search', 0, 1),
431             );
432             push @fields, map((
433             ($_ ? $pksearch_name_plural : $pksearch_name),
434             _make_query_pk_field($name, $type, \%name2pk21, $_),
435 30 100       55 ), (0, 1)) if keys %{ $name2pk21{$name} };
  30 100       153  
436 30         189 @fields;
437             } keys %name2type
438             },
439             };
440             push @ast, {
441             kind => 'type',
442             name => 'Mutation',
443             fields => {
444             map {
445 28         86 my $name = $_;
446 28         75 my $create_name = "create$name";
447 28         51 my $update_name = "update$name";
448 28         68 my $delete_name = "delete$name";
449             (
450 28         55 $create_name => _make_input_field($name, $name, 'create', 1, 1),
451             $update_name => _make_input_field($name, $name, 'update', 1, 1),
452             $delete_name => _make_input_field($name, 'Boolean', 'ID', 1, 1),
453             )
454 6         47 } grep !$name2isview{$_}, keys %name2type
455             },
456             };
457             +{
458 6         138 schema => GraphQL::Schema->from_ast(\@ast),
459             root_value => $dbic_schema,
460             resolver => \&field_resolver,
461             };
462             }
463              
464             =encoding utf-8
465              
466             =head1 NAME
467              
468             GraphQL::Plugin::Convert::DBIC - convert DBIx::Class schema to GraphQL schema
469              
470             =begin markdown
471              
472             # PROJECT STATUS
473              
474             | OS | Build status |
475             |:-------:|--------------:|
476             | Linux | [![Build Status](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-DBIC.svg?branch=master)](https://travis-ci.org/graphql-perl/GraphQL-Plugin-Convert-DBIC) |
477              
478             [![CPAN version](https://badge.fury.io/pl/GraphQL-Plugin-Convert-DBIC.svg)](https://metacpan.org/pod/GraphQL::Plugin::Convert::DBIC)
479              
480             =end markdown
481              
482             =head1 SYNOPSIS
483              
484             use GraphQL::Plugin::Convert::DBIC;
485             use Schema;
486             my $converted = GraphQL::Plugin::Convert::DBIC->to_graphql(Schema->connect);
487             print $converted->{schema}->to_doc;
488              
489             =head1 DESCRIPTION
490              
491             This module implements the L<GraphQL::Plugin::Convert> API to convert
492             a L<DBIx::Class::Schema> to L<GraphQL::Schema> etc.
493              
494             Its C<Query> type represents a guess at what fields are suitable, based
495             on providing a lookup for each type (a L<DBIx::Class::ResultSource>).
496              
497             =head2 Example
498              
499             Consider this minimal data model:
500              
501             blog:
502             id # primary key
503             articles # has_many
504             title # non null
505             language # nullable
506             article:
507             id # primary key
508             blog # foreign key to Blog
509             title # non null
510             content # nullable
511              
512             =head2 Generated Output Types
513              
514             These L<GraphQL::Type::Object> types will be generated:
515              
516             type Blog {
517             id: Int!
518             articles: [Article]
519             title: String!
520             language: String
521             }
522              
523             type Article {
524             id: Int!
525             blog: Blog
526             title: String!
527             content: String
528             }
529              
530             type Query {
531             blog(id: [Int!]!): [Blog]
532             article(id: [Int!]!): [Blog]
533             }
534              
535             Note that while the queries take a list, the return order is
536             undefined. This also applies to the mutations. If this matters, request
537             the primary key fields and use those to sort.
538              
539             =head2 Generated Input Types
540              
541             Different input types are needed for each of CRUD (Create, Read, Update,
542             Delete).
543              
544             The create one needs to have non-null fields be non-null, for idiomatic
545             GraphQL-level error-catching. The read one needs all fields nullable,
546             since this will be how searches are implemented, allowing fields to be
547             left un-searched-for. Both need to omit primary key fields. The read
548             one also needs to omit foreign key fields, since the idiomatic GraphQL
549             way for this is to request the other object, with this as a field on it,
550             then request any required fields of this.
551              
552             Meanwhile, the update and delete ones need to include the primary key
553             fields, to indicate what to mutate, and also all non-primary key fields
554             as nullable, which for update will mean leaving them unchanged, and for
555             delete is to be ignored. These input types are split into one input
556             for the primary keys, which is a full input type to allow for multiple
557             primary keys, then a wrapper input for updates, that takes one ID input,
558             and a payload that due to the same requirements, is just the search input.
559              
560             Therefore, for the above, these input types (and an updated Query,
561             and Mutation) are created:
562              
563             input BlogCreateInput {
564             title: String!
565             language: String
566             }
567              
568             input BlogSearchInput {
569             title: String
570             language: String
571             }
572              
573             input BlogIDInput {
574             id: Int!
575             }
576              
577             input BlogUpdateInput {
578             id: BlogIDInput!
579             payload: BlogSearchInput!
580             }
581              
582             input ArticleCreateInput {
583             blog_id: Int!
584             title: String!
585             content: String
586             }
587              
588             input ArticleSearchInput {
589             title: String
590             content: String
591             }
592              
593             input ArticleIDInput {
594             id: Int!
595             }
596              
597             input ArticleUpdateInput {
598             id: ArticleIDInput!
599             payload: ArticleSearchInput!
600             }
601              
602             type Mutation {
603             createBlog(input: [BlogCreateInput!]!): [Blog]
604             createArticle(input: [ArticleCreateInput!]!): [Article]
605             deleteBlog(input: [BlogIDInput!]!): [Boolean]
606             deleteArticle(input: [ArticleIDInput!]!): [Boolean]
607             updateBlog(input: [BlogUpdateInput!]!): [Blog]
608             updateArticle(input: [ArticleUpdateInput!]!): [Article]
609             }
610              
611             extends type Query {
612             searchBlog(input: BlogSearchInput!): [Blog]
613             searchArticle(input: ArticleSearchInput!): [Article]
614             }
615              
616             =head1 ARGUMENTS
617              
618             To the C<to_graphql> method: a L<DBIx::Class::Schema> object.
619              
620             =head1 PACKAGE FUNCTIONS
621              
622             =head2 field_resolver
623              
624             This is available as C<\&GraphQL::Plugin::Convert::DBIC::field_resolver>
625             in case it is wanted for use outside of the "bundle" of the C<to_graphql>
626             method.
627              
628             =head1 DEBUGGING
629              
630             To debug, set environment variable C<GRAPHQL_DEBUG> to a true value.
631              
632             =head1 AUTHOR
633              
634             Ed J, C<< <etj at cpan.org> >>
635              
636             =head1 LICENSE
637              
638             Copyright (C) Ed J
639              
640             This library is free software; you can redistribute it and/or modify
641             it under the same terms as Perl itself.
642              
643             =cut
644              
645             1;