1 package Catalyst::Controller::DBIC::API::RequestArguments;
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');
9 use Catalyst::Controller::DBIC::API::Validator;
10 use namespace::autoclean;
12 use Catalyst::Controller::DBIC::API::JoinBuilder;
14 =attribute_private search_validator
16 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
20 =attribute_private select_validator
22 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
26 =attribute_private prefetch_validator
28 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
32 has [qw( search_validator select_validator )] => (
34 isa => 'Catalyst::Controller::DBIC::API::Validator',
36 builder => '_build_validator',
39 sub _build_validator {
40 return Catalyst::Controller::DBIC::API::Validator->new;
43 parameter static => ( isa => Bool, default => 0 );
51 requires qw/check_has_relation check_column_relation prefetch_allows /;
55 requires qw/_controller check_has_relation check_column_relation/;
58 =attribute_public count is: ro, isa: Int
60 count is the number of rows to be returned during paging
67 writer => '_set_count',
69 predicate => 'has_count',
72 =attribute_public page is: ro, isa: Int
74 page is what page to return while paging
81 writer => '_set_page',
83 predicate => 'has_page',
86 =attribute_public offset is ro, isa: Int
88 offset specifies where to start the paged result (think SQL LIMIT)
95 writer => '_set_offset',
97 predicate => 'has_offset',
100 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
102 ordered_by is passed to ->search to determine sorting
109 writer => '_set_ordered_by',
111 predicate => 'has_ordered_by',
113 default => sub { $p->static ? [] : undef },
116 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
118 grouped_by is passed to ->search to determine aggregate results
125 writer => '_set_grouped_by',
127 predicate => 'has_grouped_by',
129 default => sub { $p->static ? [] : undef },
132 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
134 prefetch is passed to ->search to optimize the number of database fetches for joins
141 writer => '_set_prefetch',
143 default => sub { $p->static ? [] : undef },
147 my ($self, $new) = @_;
149 foreach my $pf (@$new)
151 if(HashRef->check($pf))
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];
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];
165 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
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.
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.
173 has 'search_exposes' =>
176 writer => '_set_search_exposes',
177 isa => ArrayRef[Str|HashRef],
178 predicate => 'has_search_exposes',
179 default => sub { [ ] },
182 my ($self, $new) = @_;
183 $self->search_validator->load($_) for @$new;
187 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
189 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
191 Please see L</generate_parameters_attributes> for details on how the format works.
198 writer => '_set_search',
199 isa => SearchParameters,
200 predicate => 'has_search',
204 my ($self, $new) = @_;
206 if($self->has_search_exposes and @{$self->search_exposes})
208 foreach my $foo (@$new)
210 while( my ($k, $v) = each %$foo)
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];
220 foreach my $foo (@$new)
222 while( my ($k, $v) = each %$foo)
224 $self->check_column_relation({$k => $v});
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);
236 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
238 search_parameters stores the formatted search parameters that will be passed to ->search
242 has search_parameters =>
245 isa => SearchParameters,
246 writer => '_set_search_parameters',
247 predicate => 'has_search_parameters',
249 default => sub { [{}] },
252 =attribute_public search_attributes is:ro, isa: HashRef
254 search_attributes stores the formatted search attributes that will be passed to ->search
258 has search_attributes =>
262 writer => '_set_search_attributes',
263 predicate => 'has_search_attributes',
267 =attribute_public search_total_entries is: ro, isa: Int
269 search_total_entries stores the total number of entries in a paged search result
273 has search_total_entries =>
277 writer => '_set_search_total_entries',
278 predicate => 'has_search_total_entries',
281 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
283 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
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.
289 has 'select_exposes' =>
292 writer => '_set_select_exposes',
293 isa => ArrayRef[Str|HashRef],
294 predicate => 'has_select_exposes',
295 default => sub { [ ] },
298 my ($self, $new) = @_;
299 $self->select_validator->load($_) for @$new;
303 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
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.
307 Please see L<DBIx::Class::ResultSet/select> for more details.
314 writer => '_set_select',
315 isa => SelectColumns,
316 predicate => 'has_select',
317 default => sub { $p->static ? [] : undef },
321 my ($self, $new) = @_;
322 if($self->has_select_exposes)
324 foreach my $val (@$new)
326 die "'$val' is not allowed in a select"
327 unless $self->select_validator->validate($val);
332 $self->check_column_relation($_, $p->static) for @$new;
337 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
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.
341 Please see L<DBIx::Class::ResultSet/as> for more details.
350 default => sub { $p->static ? [] : undef },
353 my ($self, $new) = @_;
354 if($self->has_select)
356 die "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
357 unless @$new == @{$self->select || []};
361 die "'as' is only valid if 'select is also provided'";
366 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
368 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
370 Provides a single handle which returns the 'join' attribute for search_attributes:
372 build_joins => 'joins'
383 build_joins => 'joins',
387 =attribute_public request_data is: ro, isa: HashRef
389 request_data holds the raw (but deserialized) data for ths request
393 has 'request_data' =>
397 writer => '_set_request_data',
398 predicate => 'has_request_data',
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};
416 method _build_joins => sub { return Catalyst::Controller::DBIC::API::JoinBuilder->new(name => 'TOP') };
418 =method_protected format_search_parameters
420 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
424 method format_search_parameters => sub
426 my ($self, $params) = @_;
430 foreach my $param (@$params)
432 push(@$genparams, $self->generate_column_parameters($self->stored_result_source, $param, $self->joins));
438 =method_protected generate_column_parameters
440 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
444 method generate_column_parameters => sub
446 my ($self, $source, $param, $join, $base) = @_;
448 my $search_params = {};
451 foreach my $column (keys %$param)
453 if ($source->has_relationship($column))
455 # check if the value isn't a hashref
456 unless (ref($param->{$column}) && reftype($param->{$column}) eq 'HASH')
458 $search_params->{join('.', $base, $column)} = $param->{$column};
462 $search_params = { %$search_params, %{
463 $self->generate_column_parameters
465 $source->related_source($column),
467 Catalyst::Controller::DBIC::API::JoinBuilder->new(parent => $join, name => $column),
472 elsif ($source->has_column($column))
474 $search_params->{join('.', $base, $column)} = $param->{$column};
476 # might be a sql function instead of a column name
477 # e.g. {colname => {like => '%foo%'}}
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};
485 die "$column is neither a relationship nor a column\n";
490 return $search_params;
493 =method_protected generate_parameters_attributes
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.
499 method generate_parameters_attributes => sub
501 my ($self, $args) = @_;
503 return ( $self->format_search_parameters($args), $self->search_attributes );
506 =method_protected _build_search_attributes
508 This builder method generates the search attributes
512 method _build_search_attributes => sub
514 my ($self, $args) = @_;
515 my $static = $self->_controller;
516 my $search_attributes =
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,
531 $search_attributes->{page} = $self->page;
533 elsif(!$self->has_page && defined($search_attributes->{offset}) && defined($search_attributes->{rows}))
535 $search_attributes->{page} = $search_attributes->{offset} / $search_attributes->{rows} + 1;
536 delete $search_attributes->{offset};
547 (ref($_->[1]) && reftype($_->[1]) eq 'HASH' && keys %{$_->[1]})
548 || (ref($_->[1]) && reftype($_->[1]) eq 'ARRAY' && @{$_->[1]})
553 map { [$_, $search_attributes->{$_}] }
554 keys %$search_attributes
558 if ($search_attributes->{page} && !$search_attributes->{rows}) {
559 die 'list_page can only be used with list_count';
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}];
569 return $search_attributes;
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.