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 );
50 qw( check_has_relation check_column_relation prefetch_allows );
53 requires qw( _controller check_has_relation check_column_relation );
56 =attribute_public count is: ro, isa: Int
58 count is the number of rows to be returned during paging
64 writer => '_set_count',
66 predicate => 'has_count',
69 =attribute_public page is: ro, isa: Int
71 page is what page to return while paging
77 writer => '_set_page',
79 predicate => 'has_page',
82 =attribute_public offset is ro, isa: Int
84 offset specifies where to start the paged result (think SQL LIMIT)
90 writer => '_set_offset',
92 predicate => 'has_offset',
95 =attribute_public ordered_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/OrderedBy>
97 ordered_by is passed to ->search to determine sorting
101 has 'ordered_by' => (
103 writer => '_set_ordered_by',
105 predicate => 'has_ordered_by',
107 default => sub { $p->static ? [] : undef },
110 =attribute_public groupd_by is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/GroupedBy>
112 grouped_by is passed to ->search to determine aggregate results
116 has 'grouped_by' => (
118 writer => '_set_grouped_by',
120 predicate => 'has_grouped_by',
122 default => sub { $p->static ? [] : undef },
125 =attribute_public prefetch is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/Prefetch>
127 prefetch is passed to ->search to optimize the number of database fetches for joins
133 writer => '_set_prefetch',
135 default => sub { $p->static ? [] : undef },
138 my ( $self, $new ) = @_;
140 foreach my $pf (@$new) {
141 if ( HashRef->check($pf) ) {
143 qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
144 unless $self->prefetch_validator->validate($pf)->[0];
148 qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
149 unless $self->prefetch_validator->validate(
156 =attribute_public search_exposes is: ro, isa: ArrayRef[Str|HashRef]
158 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.
160 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.
164 has 'search_exposes' => (
166 writer => '_set_search_exposes',
167 isa => ArrayRef [ Str | HashRef ],
168 predicate => 'has_search_exposes',
169 default => sub { [] },
171 my ( $self, $new ) = @_;
172 $self->search_validator->load($_) for @$new;
176 =attribute_public search is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
178 search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
180 Please see L</generate_parameters_attributes> for details on how the format works.
186 writer => '_set_search',
187 isa => SearchParameters,
188 predicate => 'has_search',
191 my ( $self, $new ) = @_;
193 if ( $self->has_search_exposes and @{ $self->search_exposes } ) {
194 foreach my $foo (@$new) {
195 while ( my ( $k, $v ) = each %$foo ) {
196 local $Data::Dumper::Terse = 1;
198 qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
199 unless $self->search_validator->validate(
205 foreach my $foo (@$new) {
206 while ( my ( $k, $v ) = each %$foo ) {
207 $self->check_column_relation( { $k => $v } );
212 my ( $search_parameters, $search_attributes ) =
213 $self->generate_parameters_attributes($new);
214 $self->_set_search_parameters($search_parameters);
215 $self->_set_search_attributes($search_attributes);
220 =attribute_public search_parameters is:ro, isa: L<Catalyst::Controller::DBIC::API::Types/SearchParameters>
222 search_parameters stores the formatted search parameters that will be passed to ->search
226 has search_parameters => (
228 isa => SearchParameters,
229 writer => '_set_search_parameters',
230 predicate => 'has_search_parameters',
232 default => sub { [ {} ] },
235 =attribute_public search_attributes is:ro, isa: HashRef
237 search_attributes stores the formatted search attributes that will be passed to ->search
241 has search_attributes => (
244 writer => '_set_search_attributes',
245 predicate => 'has_search_attributes',
249 =attribute_public search_total_entries is: ro, isa: Int
251 search_total_entries stores the total number of entries in a paged search result
255 has search_total_entries => (
258 writer => '_set_search_total_entries',
259 predicate => 'has_search_total_entries',
262 =attribute_public select_exposes is: ro, isa: ArrayRef[Str|HashRef]
264 select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
266 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.
270 has 'select_exposes' => (
272 writer => '_set_select_exposes',
273 isa => ArrayRef [ Str | HashRef ],
274 predicate => 'has_select_exposes',
275 default => sub { [] },
277 my ( $self, $new ) = @_;
278 $self->select_validator->load($_) for @$new;
282 =attribute_public select is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/SelectColumns>
284 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.
286 Please see L<DBIx::Class::ResultSet/select> for more details.
292 writer => '_set_select',
293 isa => SelectColumns,
294 predicate => 'has_select',
295 default => sub { $p->static ? [] : undef },
298 my ( $self, $new ) = @_;
299 if ( $self->has_select_exposes ) {
300 foreach my $val (@$new) {
301 die "'$val' is not allowed in a select"
302 unless $self->select_validator->validate($val);
306 $self->check_column_relation( $_, $p->static ) for @$new;
311 =attribute_public as is: ro, isa: L<Catalyst::Controller::DBIC::API::Types/AsAliases>
313 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.
315 Please see L<DBIx::Class::ResultSet/as> for more details.
323 default => sub { $p->static ? [] : undef },
325 my ( $self, $new ) = @_;
326 if ( $self->has_select ) {
328 "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
329 unless @$new == @{ $self->select || [] };
331 elsif ( defined $new ) {
332 die "'as' is only valid if 'select is also provided'";
337 =attribute_public joins is: ro, isa L<Catalyst::Controller::DBIC::API::Types/JoinBuilder>
339 joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
341 Provides a single handle which returns the 'join' attribute for search_attributes:
343 build_joins => 'joins'
351 handles => { build_joins => 'joins', }
354 =attribute_public request_data is: ro, isa: HashRef
356 request_data holds the raw (but deserialized) data for ths request
360 has 'request_data' => (
363 writer => '_set_request_data',
364 predicate => 'has_request_data',
366 my ( $self, $new ) = @_;
367 my $controller = $self->_controller;
368 return unless defined($new) && keys %$new;
369 $self->_set_prefetch( $new->{ $controller->prefetch_arg } )
370 if exists $new->{ $controller->prefetch_arg };
371 $self->_set_select( $new->{ $controller->select_arg } )
372 if exists $new->{ $controller->select_arg };
373 $self->_set_as( $new->{ $controller->as_arg } )
374 if exists $new->{ $controller->as_arg };
375 $self->_set_grouped_by( $new->{ $controller->grouped_by_arg } )
376 if exists $new->{ $controller->grouped_by_arg };
377 $self->_set_ordered_by( $new->{ $controller->ordered_by_arg } )
378 if exists $new->{ $controller->ordered_by_arg };
379 $self->_set_count( $new->{ $controller->count_arg } )
380 if exists $new->{ $controller->count_arg };
381 $self->_set_page( $new->{ $controller->page_arg } )
382 if exists $new->{ $controller->page_arg };
383 $self->_set_offset( $new->{ $controller->offset_arg } )
384 if exists $new->{ $controller->offset_arg };
385 $self->_set_search( $new->{ $controller->search_arg } )
386 if exists $new->{ $controller->search_arg };
390 method _build_joins => sub {
391 return Catalyst::Controller::DBIC::API::JoinBuilder->new(
395 =method_protected format_search_parameters
397 format_search_parameters iterates through the provided params ArrayRef, calling generate_column_parameters on each one
401 method format_search_parameters => sub {
402 my ( $self, $params ) = @_;
406 foreach my $param (@$params) {
409 $self->generate_column_parameters(
410 $self->stored_result_source,
419 =method_protected generate_column_parameters
421 generate_column_parameters recursively generates properly aliased parameters for search, building a new JoinBuilder each layer of recursion
425 method generate_column_parameters => sub {
426 my ( $self, $source, $param, $join, $base ) = @_;
428 my $search_params = {};
431 foreach my $column ( keys %$param ) {
432 if ( $source->has_relationship($column) ) {
434 # check if the value isn't a hashref
435 unless ( ref( $param->{$column} )
436 && reftype( $param->{$column} ) eq 'HASH' )
438 $search_params->{ join( '.', $base, $column ) } =
445 %{ $self->generate_column_parameters(
446 $source->related_source($column),
448 Catalyst::Controller::DBIC::API::JoinBuilder->new(
457 elsif ( $source->has_column($column) ) {
458 $search_params->{ join( '.', $base, $column ) } =
462 # might be a sql function instead of a column name
463 # e.g. {colname => {like => '%foo%'}}
465 # but only if it's not a hashref
466 unless ( ref( $param->{$column} )
467 && reftype( $param->{$column} ) eq 'HASH' )
469 $search_params->{ join( '.', $base, $column ) } =
473 die "$column is neither a relationship nor a column\n";
478 return $search_params;
481 =method_protected generate_parameters_attributes
483 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.
487 method generate_parameters_attributes => sub {
488 my ( $self, $args ) = @_;
490 return ( $self->format_search_parameters($args),
491 $self->search_attributes );
494 =method_protected _build_search_attributes
496 This builder method generates the search attributes
500 method _build_search_attributes => sub {
501 my ( $self, $args ) = @_;
502 my $static = $self->_controller;
503 my $search_attributes = {
504 group_by => $self->grouped_by
506 ( scalar( @{ $static->grouped_by } ) ) ? $static->grouped_by
509 order_by => $self->ordered_by
511 ( scalar( @{ $static->ordered_by } ) ) ? $static->ordered_by
514 select => $self->select
516 ( scalar( @{ $static->select } ) ) ? $static->select
520 || ( ( scalar( @{ $static->as } ) ) ? $static->as : undef ),
521 prefetch => $self->prefetch || $static->prefetch || undef,
522 rows => $self->count || $static->count,
523 page => $static->page,
524 offset => $self->offset,
525 join => $self->build_joins,
528 if ( $self->has_page ) {
529 $search_attributes->{page} = $self->page;
531 elsif (!$self->has_page
532 && defined( $search_attributes->{offset} )
533 && defined( $search_attributes->{rows} ) )
535 $search_attributes->{page} =
536 $search_attributes->{offset} / $search_attributes->{rows} + 1;
537 delete $search_attributes->{offset};
540 $search_attributes = {
545 && reftype( $_->[1] ) eq 'HASH'
546 && keys %{ $_->[1] } )
548 && reftype( $_->[1] ) eq 'ARRAY'
553 map { [ $_, $search_attributes->{$_} ] }
554 keys %$search_attributes
557 if ( $search_attributes->{page} && !$search_attributes->{rows} ) {
558 die 'list_page can only be used with list_count';
561 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} = [
567 map { ( Str->check($_) && $_ !~ m/\./ ) ? "me.$_" : $_ }
568 ( ref $search_attributes->{select} )
569 ? @{ $search_attributes->{select} }
570 : $search_attributes->{select}
574 return $search_attributes;
582 RequestArguments embodies those arguments that are provided as part of a request
583 or effect validation on request arguments. This Role can be consumed in one of
584 two ways. As this is a parameterized Role, it accepts a single argument at
585 composition time: 'static'. This indicates that those parameters should be
586 stored statically and used as a fallback when the current request doesn't