moved prefetch_allows to StaticArguments to fix controller instantiation failures...
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API / RequestArguments.pm
1 package Catalyst::Controller::DBIC::API::RequestArguments;
2
3 #ABSTRACT: Provides Request argument validation
4 use MooseX::Role::Parameterized;
5 use Catalyst::Controller::DBIC::API::Types(':all');
6 use MooseX::Types::Moose(':all');
7 use Scalar::Util('reftype');
8 use Data::Dumper;
9 use Catalyst::Controller::DBIC::API::Validator;
10 use namespace::autoclean;
11
12 use Catalyst::Controller::DBIC::API::JoinBuilder;
13
14 =attribute_private search_validator
15
16 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
17
18 =cut
19
20 =attribute_private select_validator
21
22 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
23
24 =cut
25
26 =attribute_private prefetch_validator
27
28 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
29
30 =cut
31
32 has [qw( search_validator select_validator )] => (
33     is => 'ro',
34     isa => 'Catalyst::Controller::DBIC::API::Validator',
35     lazy => 1,
36     builder => '_build_validator',
37 );
38
39 sub _build_validator {
40     return Catalyst::Controller::DBIC::API::Validator->new;
41 }
42
43 parameter static => ( isa => Bool, default => 0 );
44
45 role {
46
47     my $p = shift;
48
49     if($p->static)
50     {
51         requires qw/check_has_relation check_column_relation prefetch_allows /;
52     }
53     else
54     {
55         requires qw/_controller check_has_relation check_column_relation/;
56     }
57
58 =attribute_public count is: ro, isa: Int
59
60 count is the number of rows to be returned during paging
61
62 =cut
63
64     has 'count' =>
65     (
66         is => 'ro',
67         writer => '_set_count',
68         isa => Int,
69         predicate => 'has_count',
70     );
71
72 =attribute_public page is: ro, isa: Int
73
74 page is what page to return while paging
75
76 =cut
77
78     has 'page' =>
79     (
80         is => 'ro',
81         writer => '_set_page',
82         isa => Int,
83         predicate => 'has_page',
84     );
85
86 =attribute_public offset is ro, isa: Int
87
88 offset specifies where to start the paged result (think SQL LIMIT)
89
90 =cut
91
92     has 'offset' =>
93     (
94         is => 'ro',
95         writer => '_set_offset',
96         isa => Int,
97         predicate => 'has_offset',
98     );
99
100 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
101
102 ordered_by is passed to ->search to determine sorting
103
104 =cut
105
106     has 'ordered_by' =>
107     (
108         is => 'ro',
109         writer => '_set_ordered_by',
110         isa => OrderedBy,
111         predicate => 'has_ordered_by',
112         coerce => 1,
113         default => sub { $p->static ? [] : undef },
114     );
115
116 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
117
118 grouped_by is passed to ->search to determine aggregate results
119
120 =cut
121
122     has 'grouped_by' =>
123     (
124         is => 'ro',
125         writer => '_set_grouped_by',
126         isa => GroupedBy,
127         predicate => 'has_grouped_by',
128         coerce => 1,
129         default => sub { $p->static ? [] : undef },
130     );
131
132 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
133
134 prefetch is passed to ->search to optimize the number of database fetches for joins
135
136 =cut
137
138     has prefetch =>
139     (
140         is => 'ro',
141         writer => '_set_prefetch',
142         isa => Prefetch,
143         default => sub { $p->static ? [] : undef },
144         coerce => 1,
145         trigger => sub
146         {
147             my ($self, $new) = @_;
148
149             foreach my $pf (@$new)
150             {
151                 if(HashRef->check($pf))
152                 {
153                     die qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
154                         unless $self->prefetch_validator->validate($pf)->[0];
155                 }
156                 else
157                 {
158                     die qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
159                         unless $self->prefetch_validator->validate({$pf => 1})->[0];
160                 }
161             }
162         },
163     );
164
165 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
166
167 search_exposes limits what can actually be searched. If a certain column isn't indexed or perhaps a BLOB, you can explicitly say which columns can be search and exclude that one.
168
169 Like the synopsis in DBIC::API shows, you can declare a "template" of what is allowed (by using an '*'). Each element passed in, will be converted into a Data::DPath and added to the validator.
170
171 =cut
172
173     has 'search_exposes' =>
174     (
175         is => 'ro',
176         writer => '_set_search_exposes',
177         isa => ArrayRef[Str|HashRef],
178         predicate => 'has_search_exposes',
179         default => sub { [ ] },
180         trigger => sub
181         {
182             my ($self, $new) = @_;
183             $self->search_validator->load($_) for @$new;
184         },
185     );
186
187 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
188
189 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
190
191 Please see L</generate_parameters_attributes> for details on how the format works.
192
193 =cut
194
195     has 'search' =>
196     (
197         is => 'ro',
198         writer => '_set_search',
199         isa => SearchParameters,
200         predicate => 'has_search',
201         coerce => 1,
202         trigger => sub
203         {
204             my ($self, $new) = @_;
205
206             if($self->has_search_exposes and @{$self->search_exposes})
207             {
208                 foreach my $foo (@$new)
209                 {
210                     while( my ($k, $v) = each %$foo)
211                     {
212                         local $Data::Dumper::Terse = 1;
213                         die qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
214                             unless $self->search_validator->validate({$k=>$v})->[0];
215                     }
216                 }
217             }
218             else
219             {
220                 foreach my $foo (@$new)
221                 {
222                     while( my ($k, $v) = each %$foo)
223                     {
224                         $self->check_column_relation({$k => $v});
225                     }
226                 }
227             }
228
229             my ($search_parameters, $search_attributes) = $self->generate_parameters_attributes($new);
230             $self->_set_search_parameters($search_parameters);
231             $self->_set_search_attributes($search_attributes);
232
233         },
234     );
235
236 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
237
238 search_parameters stores the formatted search parameters that will be passed to ->search
239
240 =cut
241
242     has search_parameters =>
243     (
244         is => 'ro',
245         isa => SearchParameters,
246         writer => '_set_search_parameters',
247         predicate => 'has_search_parameters',
248         coerce => 1,
249         default => sub { [{}] },
250     );
251
252 =attribute_public search_attributes is:ro, isa: HashRef
253
254 search_attributes stores the formatted search attributes that will be passed to ->search
255
256 =cut
257
258     has search_attributes =>
259     (
260         is => 'ro',
261         isa => HashRef,
262         writer => '_set_search_attributes',
263         predicate => 'has_search_attributes',
264         lazy_build => 1,
265     );
266
267 =attribute_public search_total_entries is: ro, isa: Int
268
269 search_total_entries stores the total number of entries in a paged search result
270
271 =cut
272
273     has search_total_entries =>
274     (
275         is => 'ro',
276         isa => Int,
277         writer => '_set_search_total_entries',
278         predicate => 'has_search_total_entries',
279     );
280
281 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
282
283 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
284
285 Like the synopsis in DBIC::API shows, you can declare a "template" of what is allowed (by using an '*'). Each element passed in, will be converted into a Data::DPath and added to the validator.
286
287 =cut
288
289     has 'select_exposes' =>
290     (
291         is => 'ro',
292         writer => '_set_select_exposes',
293         isa => ArrayRef[Str|HashRef],
294         predicate => 'has_select_exposes',
295         default => sub { [ ] },
296         trigger => sub
297         {
298             my ($self, $new) = @_;
299             $self->select_validator->load($_) for @$new;
300         },
301     );
302
303 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
304
305 select is the search attribute that allows you to both limit what is returned in the result set, and also make use of database functions like COUNT.
306
307 Please see L<DBIx::Class::ResultSet/select> for more details.
308
309 =cut
310
311     has select =>
312     (
313         is => 'ro',
314         writer => '_set_select',
315         isa => SelectColumns,
316         predicate => 'has_select',
317         default => sub { $p->static ? [] : undef },
318         coerce => 1,
319         trigger => sub
320         {
321             my ($self, $new) = @_;
322             if($self->has_select_exposes)
323             {
324                 foreach my $val (@$new)
325                 {
326                     die "'$val' is not allowed in a select"
327                         unless $self->select_validator->validate($val);
328                 }
329             }
330             else
331             {
332                 $self->check_column_relation($_, $p->static) for @$new;
333             }
334         },
335     );
336
337 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
338
339 as is the search attribute compliment to L</select> that allows you to label columns for object inflaction and actually reference database functions like COUNT.
340
341 Please see L<DBIx::Class::ResultSet/as> for more details.
342
343 =cut
344
345     has as =>
346     (
347         is => 'ro',
348         writer => '_set_as',
349         isa => AsAliases,
350         default => sub { $p->static ? [] : undef },
351         trigger => sub
352         {
353             my ($self, $new) = @_;
354             if($self->has_select)
355             {
356                 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
357                     unless @$new == @{$self->select || []};
358             }
359             elsif(defined $new)
360             {
361                 die "'as' is only valid if 'select is also provided'";
362             }
363         }
364     );
365
366 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
367
368 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
369
370 Provides a single handle which returns the 'join' attribute for search_attributes:
371
372     build_joins => 'joins'
373
374 =cut
375
376     has joins =>
377     (
378         is => 'ro',
379         isa => JoinBuilder,
380         lazy_build => 1,
381         handles =>
382         {
383             build_joins => 'joins',
384         }
385     );
386
387 =attribute_public request_data is: ro, isa: HashRef
388
389 request_data holds the raw (but deserialized) data for ths request
390
391 =cut
392
393     has 'request_data' =>
394     (
395         is => 'ro',
396         isa => HashRef,
397         writer => '_set_request_data',
398         predicate => 'has_request_data',
399         trigger => sub
400         {
401             my ($self, $new) = @_;
402             my $controller = $self->_controller;
403             return unless defined($new) && keys %$new;
404             $self->_set_prefetch($new->{$controller->prefetch_arg}) if exists $new->{$controller->prefetch_arg};
405             $self->_set_select($new->{$controller->select_arg}) if exists $new->{$controller->select_arg};
406             $self->_set_as($new->{$controller->as_arg}) if exists $new->{$controller->as_arg};
407             $self->_set_grouped_by($new->{$controller->grouped_by_arg}) if exists $new->{$controller->grouped_by_arg};
408             $self->_set_ordered_by($new->{$controller->ordered_by_arg}) if exists $new->{$controller->ordered_by_arg};
409             $self->_set_count($new->{$controller->count_arg}) if exists $new->{$controller->count_arg};
410             $self->_set_page($new->{$controller->page_arg}) if exists $new->{$controller->page_arg};
411             $self->_set_offset($new->{$controller->offset_arg}) if exists $new->{$controller->offset_arg};
412             $self->_set_search($new->{$controller->search_arg}) if exists $new->{$controller->search_arg};
413         }
414     );
415
416     method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
417
418 =method_protected format_search_parameters
419
420 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
421
422 =cut
423
424     method format_search_parameters => sub
425     {
426         my ($self, $params) = @_;
427
428         my $genparams = [];
429
430         foreach my $param (@$params)
431         {
432             push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
433         }
434
435         return $genparams;
436     };
437
438 =method_protected generate_column_parameters
439
440 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
441
442 =cut
443
444     method generate_column_parameters => sub
445     {
446         my ($self, $source, $param, $join, $base) = @_;
447         $base ||= 'me';
448         my $search_params = {};
449
450         # build up condition
451         foreach my $column (keys %$param)
452         {
453             if ($source->has_relationship($column))
454             {
455                 # check if the value isn't a hashref
456                 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
457                 {
458                     $search_params->{join('.', $base, $column)} = $param->{$column};
459                     next;
460                 }
461
462                 $search_params = { %$search_params, %{
463                     $self->generate_column_parameters
464                     (
465                         $source->related_source($column),
466                         $param->{$column},
467                         Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
468                         $column
469                     )
470                 }};
471             }
472             elsif ($source->has_column($column))
473             {
474                 $search_params->{join('.', $base, $column)} = $param->{$column};
475             }
476             # might be a sql function instead of a column name
477             # e.g. {colname => {like => '%foo%'}}
478             else
479             {
480                 # but only if it's not a hashref
481                 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH') {
482                     $search_params->{join('.', $base, $column)} = $param->{$column};
483                 }
484                 else {
485                     die "$column is neither a relationship nor a column\n";
486                 }
487             }
488         }
489
490         return $search_params;
491     };
492
493 =method_protected generate_parameters_attributes
494
495 generate_parameters_attributes takes the raw search arguments and formats the parameters by calling format_search_parameters. Then builds the related attributes, preferring request-provided arguments for things like grouped_by over statically configured options. Finally tacking on the appropriate joins. Returns both formatted search parameters and the search attributes.
496
497 =cut
498
499     method generate_parameters_attributes => sub
500     {
501         my ($self, $args) = @_;
502
503         return ( $self->format_search_parameters($args), $self->search_attributes );
504     };
505
506 =method_protected _build_search_attributes
507
508 This builder method generates the search attributes
509
510 =cut
511
512     method _build_search_attributes => sub
513     {
514         my ($self, $args) = @_;
515         my $static = $self->_controller;
516         my $search_attributes =
517         {
518             group_by => $self->grouped_by || ((scalar(@{$static->grouped_by})) ? $static->grouped_by : undef),
519             order_by => $self->ordered_by || ((scalar(@{$static->ordered_by})) ? $static->ordered_by : undef),
520             select => $self->select || ((scalar(@{$static->select})) ? $static->select : undef),
521             as => $self->as || ((scalar(@{$static->as})) ? $static->as : undef),
522             prefetch => $self->prefetch || $static->prefetch || undef,
523             rows => $self->count || $static->count,
524             page => $static->page,
525             offset => $self->offset,
526             join => $self->build_joins,
527         };
528
529         if($self->has_page)
530         {
531             $search_attributes->{page} = $self->page;
532         }
533         elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
534         {
535             $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
536             delete $search_attributes->{offset};
537         }
538
539
540         $search_attributes =
541         {
542             map { @$_ }
543             grep
544             {
545                 defined($_->[1])
546                 ?
547                     (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
548                     || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
549                     || length($_->[1])
550                 :
551                     undef
552             }
553             map { [$_, $search_attributes->{$_}] }
554             keys %$search_attributes
555         };
556
557
558         if ($search_attributes->{page} && !$search_attributes->{rows}) {
559             die 'list_page can only be used with list_count';
560         }
561
562         if ($search_attributes->{select}) {
563             # make sure all columns have an alias to avoid ambiguous issues
564             # but allow non strings (eg. hashrefs for db procs like 'count')
565             # to pass through unmolested
566             $search_attributes->{select} = [map { (Str->check($_) && $_ !~ m/\./) ? "me.$_" : $_ } (ref $search_attributes->{select}) ? @{$search_attributes->{select}} : $search_attributes->{select}];
567         }
568
569         return $search_attributes;
570
571     };
572
573 };
574 =head1 DESCRIPTION
575
576 RequestArguments embodies those arguments that are provided as part of a request or effect validation on request arguments. This Role can be consumed in one of two ways. As this is a parameterized Role, it accepts a single argument at composition time: 'static'. This indicates that those parameters should be stored statically and used as a fallback when the current request doesn't provide them.
577
578 =cut
579
580
581 1;