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