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