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