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