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
21 =attribute_private select_validator
23 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate
28 =attribute_private prefetch_validator
30 A Catalyst::Controller::DBIC::API::Validator instance used solely to validate
35 has [qw( search_validator select_validator )] => (
37 isa => 'Catalyst::Controller::DBIC::API::Validator',
39 builder => '_build_validator',
42 sub _build_validator {
43 return Catalyst::Controller::DBIC::API::Validator->new;
46 parameter static => ( isa => Bool, default => 0 );
53 qw( check_has_relation check_column_relation prefetch_allows );
56 requires qw( _controller check_has_relation check_column_relation );
59 =attribute_public count
61 The number of rows to be returned during paging.
67 writer => '_set_count',
69 predicate => 'has_count',
72 =attribute_public page
74 What page to return while paging.
80 writer => '_set_page',
82 predicate => 'has_page',
85 =attribute_public offset
87 Specifies where to start the paged result (think SQL LIMIT).
93 writer => '_set_offset',
95 predicate => 'has_offset',
98 =attribute_public ordered_by
100 Is passed to ->search to determine sorting.
104 has 'ordered_by' => (
106 writer => '_set_ordered_by',
108 predicate => 'has_ordered_by',
110 default => sub { $p->static ? [] : undef },
113 =attribute_public groupd_by
115 Is passed to ->search to determine aggregate results.
119 has 'grouped_by' => (
121 writer => '_set_grouped_by',
123 predicate => 'has_grouped_by',
125 default => sub { $p->static ? [] : undef },
128 =attribute_public prefetch
130 Is passed to ->search to optimize the number of database fetches for joins.
136 writer => '_set_prefetch',
138 default => sub { $p->static ? [] : undef },
141 my ( $self, $new ) = @_;
143 foreach my $pf (@$new) {
144 if ( HashRef->check($pf) ) {
146 qq|'${\Dumper($pf)}' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
147 unless $self->prefetch_validator->validate($pf)->[0];
151 qq|'$pf' is not an allowed prefetch in: ${\join("\n", @{$self->prefetch_validator->templates})}|
152 unless $self->prefetch_validator->validate(
159 =attribute_public search_exposes
161 Limits what can actually be searched. If a certain column isn't indexed or
162 perhaps a BLOB, you can explicitly say which columns can be search to exclude
165 Like the synopsis in DBIC::API shows, you can declare a "template" of what is
166 allowed (by using '*'). Each element passed in, will be converted into a
167 Data::DPath and added to the validator.
171 has 'search_exposes' => (
173 writer => '_set_search_exposes',
174 isa => ArrayRef [ Str | HashRef ],
175 predicate => 'has_search_exposes',
176 default => sub { [] },
178 my ( $self, $new ) = @_;
179 $self->search_validator->load($_) for @$new;
183 =attribute_public search
185 Contains the raw search parameters. Upon setting, a trigger will fire to format
186 them, set search_parameters and search_attributes.
188 Please see L</generate_parameters_attributes> for details on how the format works.
194 writer => '_set_search',
195 isa => SearchParameters,
196 predicate => 'has_search',
199 my ( $self, $new ) = @_;
201 if ( $self->has_search_exposes and @{ $self->search_exposes } ) {
202 foreach my $foo (@$new) {
203 while ( my ( $k, $v ) = each %$foo ) {
204 local $Data::Dumper::Terse = 1;
206 qq|{ $k => ${\Dumper($v)} } is not an allowed search term in: ${\join("\n", @{$self->search_validator->templates})}|
207 unless $self->search_validator->validate(
213 foreach my $foo (@$new) {
214 while ( my ( $k, $v ) = each %$foo ) {
215 $self->check_column_relation( { $k => $v } );
220 my ( $search_parameters, $search_attributes ) =
221 $self->generate_parameters_attributes($new);
222 $self->_set_search_parameters($search_parameters);
223 $self->_set_search_attributes($search_attributes);
228 =attribute_public search_parameters
230 Stores the formatted search parameters that will be passed to ->search.
234 has search_parameters => (
236 isa => SearchParameters,
237 writer => '_set_search_parameters',
238 predicate => 'has_search_parameters',
240 default => sub { [ {} ] },
243 =attribute_public search_attributes
245 Stores the formatted search attributes that will be passed to ->search.
249 has search_attributes => (
252 writer => '_set_search_attributes',
253 predicate => 'has_search_attributes',
257 =attribute_public search_total_entries
259 Stores the total number of entries in a paged search result.
263 has search_total_entries => (
266 writer => '_set_search_total_entries',
267 predicate => 'has_search_total_entries',
270 =attribute_public select_exposes
272 Limits what can actually be selected. Use this to whitelist database functions
275 Like the synopsis in DBIC::API shows, you can declare a "template" of what is
276 allowed (by using '*'). Each element passed in, will be converted into a
277 Data::DPath and added to the validator.
281 has 'select_exposes' => (
283 writer => '_set_select_exposes',
284 isa => ArrayRef [ Str | HashRef ],
285 predicate => 'has_select_exposes',
286 default => sub { [] },
288 my ( $self, $new ) = @_;
289 $self->select_validator->load($_) for @$new;
293 =attribute_public select
295 Is the search attribute that allows you to both limit what is returned in the
296 result set and also make use of database functions like COUNT.
298 Please see L<DBIx::Class::ResultSet/select> for more details.
304 writer => '_set_select',
305 isa => SelectColumns,
306 predicate => 'has_select',
307 default => sub { $p->static ? [] : undef },
310 my ( $self, $new ) = @_;
311 if ( $self->has_select_exposes ) {
312 foreach my $val (@$new) {
313 die "'$val' is not allowed in a select"
314 unless $self->select_validator->validate($val);
318 $self->check_column_relation( $_, $p->static ) for @$new;
325 Is the search attribute compliment to L</select> that allows you to label
326 columns for object inflaction and actually reference database functions like
329 Please see L<DBIx::Class::ResultSet/as> for more details.
337 default => sub { $p->static ? [] : undef },
339 my ( $self, $new ) = @_;
340 if ( $self->has_select ) {
342 "'as' argument count (${\scalar(@$new)}) must match 'select' argument count (${\scalar(@{$self->select || []})})"
343 unless @$new == @{ $self->select || [] };
345 elsif ( defined $new ) {
346 die "'as' is only valid if 'select is also provided'";
351 =attribute_public joins
353 Holds the top level JoinBuilder object used to keep track of joins automagically
354 while formatting complex search parameters.
356 Provides the method 'build_joins' which returns the 'join' attribute for
365 handles => { build_joins => 'joins', }
368 =attribute_public request_data
370 Holds the raw (but deserialized) data for this request.
374 has 'request_data' => (
377 writer => '_set_request_data',
378 predicate => 'has_request_data',
380 my ( $self, $new ) = @_;
381 my $controller = $self->_controller;
382 return unless defined($new) && keys %$new;
383 $self->_set_prefetch( $new->{ $controller->prefetch_arg } )
384 if exists $new->{ $controller->prefetch_arg };
385 $self->_set_select( $new->{ $controller->select_arg } )
386 if exists $new->{ $controller->select_arg };
387 $self->_set_as( $new->{ $controller->as_arg } )
388 if exists $new->{ $controller->as_arg };
389 $self->_set_grouped_by( $new->{ $controller->grouped_by_arg } )
390 if exists $new->{ $controller->grouped_by_arg };
391 $self->_set_ordered_by( $new->{ $controller->ordered_by_arg } )
392 if exists $new->{ $controller->ordered_by_arg };
393 $self->_set_count( $new->{ $controller->count_arg } )
394 if exists $new->{ $controller->count_arg };
395 $self->_set_page( $new->{ $controller->page_arg } )
396 if exists $new->{ $controller->page_arg };
397 $self->_set_offset( $new->{ $controller->offset_arg } )
398 if exists $new->{ $controller->offset_arg };
399 $self->_set_search( $new->{ $controller->search_arg } )
400 if exists $new->{ $controller->search_arg };
404 method _build_joins => sub {
405 return Catalyst::Controller::DBIC::API::JoinBuilder->new(
409 =method_protected format_search_parameters
411 Iterates through the provided arrayref calling generate_column_parameters on
416 method format_search_parameters => sub {
417 my ( $self, $params ) = @_;
421 foreach my $param (@$params) {
424 $self->generate_column_parameters(
425 $self->stored_result_source,
434 =method_protected generate_column_parameters
436 Recursively generates properly aliased parameters for search building a new
437 JoinBuilder each layer of recursion.
441 method generate_column_parameters => sub {
442 my ( $self, $source, $param, $join, $base ) = @_;
444 my $search_params = {};
446 # return non-hashref params unaltered
448 unless ref $param eq 'HASH';
451 foreach my $column ( keys %$param ) {
452 my $value = $param->{$column};
453 if ( $source->has_relationship($column) ) {
455 # check if the value isn't a hashref
456 unless ( ref $value eq 'HASH' )
458 $search_params->{ join( '.', $base, $column ) } =
465 %{ $self->generate_column_parameters(
466 $source->related_source($column),
468 Catalyst::Controller::DBIC::API::JoinBuilder->new(
477 elsif ( $source->has_column($column) ) {
478 $search_params->{ join( '.', $base, $column ) } =
481 elsif ( $column eq '-or' || $column eq '-and' || $column eq '-not' ) {
482 # either an arrayref or hashref
483 if ( ref $value eq 'HASH' ) {
484 $search_params->{$column} = $self->generate_column_parameters(
491 elsif ( ref $value eq 'ARRAY' ) {
492 push @{$search_params->{$column}},
493 $self->generate_column_parameters(
502 die "unsupported value '$value' for column '$column'\n";
506 # might be a sql function instead of a column name
507 # e.g. {colname => {like => '%foo%'}}
509 # but only if it's not a hashref
510 unless ( ref $value eq 'HASH' ) {
511 $search_params->{ join( '.', $base, $column ) } =
515 die "unsupported value '$value' for column '$column'\n";
520 return $search_params;
523 =method_protected generate_parameters_attributes
525 Takes the raw search arguments and formats them by calling
526 format_search_parameters. Then builds the related attributes, preferring
527 request-provided arguments for things like grouped_by over statically configured
528 options. Finally tacking on the appropriate joins.
530 Returns a list of both formatted search parameters and attributes.
534 method generate_parameters_attributes => sub {
535 my ( $self, $args ) = @_;
537 return ( $self->format_search_parameters($args),
538 $self->search_attributes );
541 method _build_search_attributes => sub {
542 my ( $self, $args ) = @_;
543 my $static = $self->_controller;
544 my $search_attributes = {
545 group_by => $self->grouped_by
547 ( scalar( @{ $static->grouped_by } ) ) ? $static->grouped_by
550 order_by => $self->ordered_by
552 ( scalar( @{ $static->ordered_by } ) ) ? $static->ordered_by
555 select => $self->select
557 ( scalar( @{ $static->select } ) ) ? $static->select
561 || ( ( scalar( @{ $static->as } ) ) ? $static->as : undef ),
562 prefetch => $self->prefetch || $static->prefetch || undef,
563 rows => $self->count || $static->count,
564 page => $static->page,
565 offset => $self->offset,
566 join => $self->build_joins,
569 if ( $self->has_page ) {
570 $search_attributes->{page} = $self->page;
572 elsif (!$self->has_page
573 && defined( $search_attributes->{offset} )
574 && defined( $search_attributes->{rows} ) )
576 $search_attributes->{page} =
577 $search_attributes->{offset} / $search_attributes->{rows} + 1;
578 delete $search_attributes->{offset};
581 $search_attributes = {
586 && reftype( $_->[1] ) eq 'HASH'
587 && keys %{ $_->[1] } )
589 && reftype( $_->[1] ) eq 'ARRAY'
594 map { [ $_, $search_attributes->{$_} ] }
595 keys %$search_attributes
598 if ( $search_attributes->{page} && !$search_attributes->{rows} ) {
599 die 'list_page can only be used with list_count';
602 if ( $search_attributes->{select} ) {
604 # make sure all columns have an alias to avoid ambiguous issues
605 # but allow non strings (eg. hashrefs for db procs like 'count')
606 # to pass through unmolested
607 $search_attributes->{select} = [
608 map { ( Str->check($_) && $_ !~ m/\./ ) ? "me.$_" : $_ }
609 ( ref $search_attributes->{select} )
610 ? @{ $search_attributes->{select} }
611 : $search_attributes->{select}
615 return $search_attributes;
623 RequestArguments embodies those arguments that are provided as part of a request
624 or effect validation on request arguments. This Role can be consumed in one of
625 two ways. As this is a parameterized Role, it accepts a single argument at
626 composition time: 'static'. This indicates that those parameters should be
627 stored statically and used as a fallback when the current request doesn't