moved prefetch_allows to StaticArguments to fix controller instantiation failures...
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API / RequestArguments.pm
CommitLineData
d2739840 1package Catalyst::Controller::DBIC::API::RequestArguments;
2
3#ABSTRACT: Provides Request argument validation
4use MooseX::Role::Parameterized;
5use Catalyst::Controller::DBIC::API::Types(':all');
6use MooseX::Types::Moose(':all');
7use Scalar::Util('reftype');
8use Data::Dumper;
28098e5d 9use Catalyst::Controller::DBIC::API::Validator;
d2739840 10use namespace::autoclean;
11
12use Catalyst::Controller::DBIC::API::JoinBuilder;
13
d2739840 14=attribute_private search_validator
15
16A Catalyst::Controller::DBIC::API::Validator instance used solely to validate search parameters
17
18=cut
19
d2739840 20=attribute_private select_validator
21
22A Catalyst::Controller::DBIC::API::Validator instance used solely to validate select parameters
23
24=cut
25
d2739840 26=attribute_private prefetch_validator
27
28A Catalyst::Controller::DBIC::API::Validator instance used solely to validate prefetch parameters
29
30=cut
31
4e5983f2 32has [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
39sub _build_validator {
40 return Catalyst::Controller::DBIC::API::Validator->new;
41}
d2739840 42
43parameter static => ( isa => Bool, default => 0 );
44
45role {
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
60count 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
74page 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
88offset 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
102ordered_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
118grouped_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
134prefetch 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
167search_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
169Like 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
189search contains the raw search parameters. Upon setting, a trigger will fire to format them, set search_parameters, and set search_attributes.
190
191Please 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
238search_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
254search_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
269search_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
283select_exposes limits what can actually be selected. Use this to whitelist database functions (such as COUNT).
284
285Like 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
305select 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
307Please 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
339as 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
341Please 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
368joins holds the top level JoinBuilder object used to keep track of joins automagically while formatting complex search parameters.
369
370Provides 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
389request_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
420format_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
440generate_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
495generate_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
508This 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
576RequestArguments 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
5811;