File Coverage

blib/lib/Catalyst/Controller/DBIC/API.pm
Criterion Covered Total %
statement 279 311 89.7
branch 72 98 73.4
condition 34 53 64.1
subroutine 72 72 100.0
pod 31 31 100.0
total 488 565 86.3


line stmt bran cond sub pod time code
1             package Catalyst::Controller::DBIC::API;
2             $Catalyst::Controller::DBIC::API::VERSION = '2.008001';
3             #ABSTRACT: Provides a DBIx::Class web service automagically
4 16     16   11649 use Moose;
  16         42  
  16         98  
5 16     16   103491 BEGIN { extends 'Catalyst::Controller'; }
6              
7 16     16   112104 use CGI::Expand ();
  16         25165  
  16         429  
8 16     16   7677 use DBIx::Class::ResultClass::HashRefInflator;
  16         5314  
  16         548  
9 16     16   131 use JSON::MaybeXS ();
  16         41  
  16         346  
10 16     16   7256 use Test::Deep::NoTest('eq_deeply');
  16         145144  
  16         112  
11 16     16   3219 use MooseX::Types::Moose(':all');
  16         40  
  16         205  
12 16     16   139540 use Moose::Util;
  16         42  
  16         156  
13 16     16   3191 use Scalar::Util( 'blessed', 'reftype' );
  16         59  
  16         887  
14 16     16   105 use Try::Tiny;
  16         41  
  16         1066  
15 16     16   9081 use Catalyst::Controller::DBIC::API::Request;
  16         82  
  16         921  
16 16     16   13122 use DBIx::Class::ResultSet::RecursiveUpdate;
  16         534758  
  16         846  
17 16     16   161 use namespace::autoclean;
  16         45  
  16         176  
18              
19             has '_json' => (
20             is => 'ro',
21             isa => JSON::MaybeXS::JSON(),
22             lazy_build => 1,
23             );
24              
25             sub _build__json {
26              
27             # no ->utf8 here because the request params get decoded by Catalyst
28 11     11   135 return JSON::MaybeXS->new;
29             }
30              
31             with 'Catalyst::Controller::DBIC::API::StoredResultSource',
32             'Catalyst::Controller::DBIC::API::StaticArguments';
33              
34             with 'Catalyst::Controller::DBIC::API::RequestArguments' => { static => 1 };
35              
36             __PACKAGE__->config();
37              
38              
39              
40             sub begin : Private {
41 106     106 1 7357946 my ( $self, $c ) = @_;
42              
43 106         496 Moose::Util::ensure_all_roles( $c->req,
44             'Catalyst::Controller::DBIC::API::Request' );
45 16     16   3347 }
  16         42  
  16         177  
46              
47              
48             sub setup : Chained('specify.in.subclass.config') : CaptureArgs(0) :
49 16     16 1 24171 PathPart('specify.in.subclass.config') { }
  16     103   43  
  16         328  
50              
51              
52             sub deserialize : Chained('setup') : CaptureArgs(0) : PathPart('') :
53             ActionClass('Deserialize') {
54 106     106 1 305378 my ( $self, $c ) = @_;
55 106         287 my $req_params;
56              
57 106 100 100     450 if ( $c->req->data && scalar( keys %{ $c->req->data } ) ) {
  17         1047  
58 16         979 $req_params = $c->req->data;
59             }
60             else {
61 90         6551 $req_params = CGI::Expand->expand_hash( $c->req->params );
62              
63 90         15919 foreach my $param (
64 90         4180 @{ [ $self->search_arg, $self->count_arg,
65             $self->page_arg, $self->offset_arg,
66             $self->ordered_by_arg, $self->grouped_by_arg,
67             $self->prefetch_arg
68             ]
69             }
70             )
71             {
72             # these params can also be composed of JSON
73             # but skip if the parameter is not provided
74 630 100       2412 next if not exists $req_params->{$param};
75              
76             # find out if CGI::Expand was involved
77 66 100       309 if ( ref $req_params->{$param} eq 'HASH' ) {
78 25         64 for my $key ( keys %{ $req_params->{$param} } ) {
  25         144  
79              
80             # copy the value because JSON::XS will alter it
81             # even if decoding failed
82 27         121 my $value = $req_params->{$param}->{$key};
83             try {
84 27     27   2316 my $deserialized = $self->_json->decode($value);
85 1         4 $req_params->{$param}->{$key} = $deserialized;
86             }
87             catch {
88 26 50   26   497 $c->log->debug(
89             "Param '$param.$key' did not deserialize appropriately: $_"
90             ) if $c->debug;
91             }
92 27         297 }
93             }
94             else {
95             try {
96 41     41   1974 my $value = $req_params->{$param};
97 41         1555 my $deserialized = $self->_json->decode($value);
98 14         57 $req_params->{$param} = $deserialized;
99             }
100             catch {
101 27 50   27   473 $c->log->debug(
102             "Param '$param' did not deserialize appropriately: $_"
103             ) if $c->debug;
104             }
105 41         423 }
106             }
107             }
108              
109 106         1778 $self->inflate_request( $c, $req_params );
110 16     16   23118 }
  16         50  
  16         94  
111              
112              
113             sub generate_rs {
114 97     97 1 3735 my ( $self, $c ) = @_;
115              
116 97   66     452 return $c->model( $c->stash->{class} || $self->class );
117             }
118              
119              
120             sub inflate_request {
121 106     106 1 502 my ( $self, $c, $params ) = @_;
122              
123             try {
124             # set static arguments
125 106     106   5228 $c->req->_set_controller($self);
126              
127             # set request arguments
128 106         493 $c->req->_set_request_data($params);
129              
130             # set the current resultset
131 97         453 $c->req->_set_current_result_set( $self->generate_rs($c) );
132              
133             }
134             catch {
135 10     10   239434 $c->log->error($_);
136 10         475 $self->push_error( $c, { message => $_ } );
137 10         999 $c->detach();
138             }
139 106         1153 }
140              
141              
142             sub object_with_id : Chained('deserialize') : CaptureArgs(1) : PathPart('') {
143 23     23 1 12095 my ( $self, $c, $id ) = @_;
144              
145 23         96 my $vals = $c->req->request_data->{ $self->data_root };
146 23 50       115 unless ( defined($vals) ) {
147              
148             # no data root, assume the request_data itself is the payload
149 23         96 $vals = $c->req->request_data;
150             }
151              
152             try {
153             # there can be only one set of data
154 23     23   997 $c->req->add_object( [ $self->object_lookup( $c, $id ), $vals ] );
155             }
156             catch {
157 8     8   169 $c->log->error($_);
158 8         391 $self->push_error( $c, { message => $_ } );
159 8         722 $c->detach();
160             }
161 16     16   21681 }
  16         60  
  16         108  
  23         232  
162              
163              
164             sub objects_no_id : Chained('deserialize') : CaptureArgs(0) : PathPart('') {
165 43     43 1 21929 my ( $self, $c ) = @_;
166              
167 43 50       190 if ( $c->req->has_request_data ) {
168 43         160 my $data = $c->req->request_data;
169 43         92 my $vals;
170              
171 43 100 66     1705 if ( exists( $data->{ $self->data_root } )
172             && defined( $data->{ $self->data_root } ) )
173             {
174 9         299 my $root = $data->{ $self->data_root };
175 9 50       55 if ( reftype($root) eq 'ARRAY' ) {
    0          
176 9         27 $vals = $root;
177             }
178             elsif ( reftype($root) eq 'HASH' ) {
179 0         0 $vals = [$root];
180             }
181             else {
182 0         0 $c->log->error('Invalid request data');
183 0         0 $self->push_error( $c,
184             { message => 'Invalid request data' } );
185 0         0 $c->detach();
186             }
187             }
188             else {
189             # no data root, assume the request_data itself is the payload
190 34         128 $vals = [ $c->req->request_data ];
191             }
192              
193 43         207 foreach my $val (@$vals) {
194 55 100       370 unless ( exists( $val->{id} ) ) {
195 38         135 $c->req->add_object(
196             [ $c->req->current_result_set->new_result( {} ), $val ] );
197 38         311 next;
198             }
199              
200             try {
201             $c->req->add_object(
202 17     17   758 [ $self->object_lookup( $c, $val->{id} ), $val ] );
203             }
204             catch {
205 2     2   40 $c->log->error($_);
206 2         82 $self->push_error( $c, { message => $_ } );
207 2         157 $c->detach();
208             }
209 17         137 }
210             }
211 16     16   21026 }
  16         46  
  16         95  
212              
213              
214             sub object_lookup {
215 40     40 1 1534 my ( $self, $c, $id ) = @_;
216              
217 40 50 33     320 die 'No valid ID provided for look up' unless defined $id and length $id;
218 40         152 my $object = $c->req->current_result_set->find($id);
219 40 100       162185 die "No object found for id '$id'" unless defined $object;
220 30         1663 return $object;
221             }
222              
223              
224             sub list {
225 54     54 1 599 my ( $self, $c ) = @_;
226              
227 54         309 $self->list_munge_parameters($c);
228 54         294 $self->list_perform_search($c);
229 53         1662 $self->list_format_output($c);
230              
231             # make sure there are no objects lingering
232 49         1203 $c->req->clear_objects();
233             }
234              
235              
236       52 1   sub list_munge_parameters { } # noop by default
237              
238              
239             sub list_perform_search {
240 54     54 1 169 my ( $self, $c ) = @_;
241              
242             try {
243 54     54   2504 my $req = $c->req;
244              
245 54         4041 my $rs =
246             $req->current_result_set->search( $req->search_parameters,
247             $req->search_attributes );
248              
249 53         27220 $req->_set_current_result_set($rs);
250              
251             $req->_set_search_total_entries(
252             $req->current_result_set->pager->total_entries )
253             if $req->has_search_attributes
254             && ( exists( $req->search_attributes->{page} )
255             && defined( $req->search_attributes->{page} )
256 53 50 66     3687 && length( $req->search_attributes->{page} ) );
      66        
      66        
257             }
258             catch {
259 1     1   20 $c->log->error($_);
260 1         41 $self->push_error( $c,
261             { message => 'a database error has occured.' } );
262 1         85 $c->detach();
263             }
264 54         542 }
265              
266              
267             sub list_format_output {
268 53     53 1 189 my ( $self, $c ) = @_;
269              
270 53         237 my $rs = $c->req->current_result_set->search;
271 53 50       15236 $rs->result_class( $self->result_class ) if $self->result_class;
272              
273             try {
274 53     53   2315 my $output = {};
275 53         174 my $formatted = [];
276              
277 53         305 foreach my $row ( $rs->all ) {
278 147         271082 push( @$formatted, $self->row_format_output( $c, $row ) );
279             }
280              
281 49         2689 $output->{ $self->data_root } = $formatted;
282              
283 49 100       253 if ( $c->req->has_search_total_entries ) {
284 11         41 $output->{ $self->total_entries_arg } =
285             $c->req->search_total_entries + 0;
286             }
287              
288 49         219 $c->stash->{ $self->stash_key } = $output;
289             }
290             catch {
291 4     4   38988 $c->log->error($_);
292 4         182 $self->push_error( $c,
293             { message => 'a database error has occured.' } );
294 4         360 $c->detach();
295             }
296 53         2640 }
297              
298              
299             sub row_format_output {
300              
301             #my ($self, $c, $row) = @_;
302 147     147 1 394 my ( $self, undef, $row ) = @_;
303 147         356 return $row; # passthrough by default
304             }
305              
306              
307             sub item {
308 3     3 1 30 my ( $self, $c ) = @_;
309              
310 3 50       14 if ( $c->req->count_objects != 1 ) {
311 0         0 $c->log->error($_);
312 0         0 $self->push_error( $c,
313             { message => 'No objects on which to operate' } );
314 0         0 $c->detach();
315             }
316             else {
317 3         49 $c->stash->{ $self->stash_key }->{ $self->item_root } =
318             $self->each_object_inflate( $c, $c->req->get_object(0)->[0] );
319             }
320             }
321              
322              
323             sub update_or_create {
324 24     24 1 88 my ( $self, $c ) = @_;
325              
326 24 50       84 if ( $c->req->has_objects ) {
327 24         154 $self->validate_objects($c);
328 19     19   528 $self->transact_objects( $c, sub { $self->save_objects( $c, @_ ) } );
  19         10910  
329             }
330             else {
331 0         0 $c->log->error($_);
332 0         0 $self->push_error( $c,
333             { message => 'No objects on which to operate' } );
334 0         0 $c->detach();
335             }
336             }
337              
338              
339             sub transact_objects {
340 23     23 1 90 my ( $self, $c, $coderef ) = @_;
341              
342             try {
343 23     23   1012 $self->stored_result_source->schema->txn_do( $coderef,
344             $c->req->objects );
345             }
346             catch {
347 3     3   14572 $c->log->error($_);
348 3         147 $self->push_error( $c,
349             { message => 'a database error has occured.' } );
350 3         260 $c->detach();
351             }
352 23         171 }
353              
354              
355             sub validate_objects {
356 24     24 1 81 my ( $self, $c ) = @_;
357              
358             try {
359 24     24   1032 foreach my $obj ( $c->req->all_objects ) {
360 32         172 $obj->[1] = $self->validate_object( $c, $obj );
361             }
362             }
363             catch {
364 5     5   87 my $err = $_;
365 5         31 $c->log->error($err);
366 5         238 $err =~ s/\s+at\s+.+\n$//g;
367 5         51 $self->push_error( $c, { message => $err } );
368 5         396 $c->detach();
369             }
370 24         249 }
371              
372              
373             sub validate_object {
374 32     32 1 99 my ( $self, $c, $obj ) = @_;
375 32         86 my ( $object, $params ) = @$obj;
376              
377 32         66 my %values;
378 15         140 my %requires_map = map { $_ => 1 } @{
379 32         62 ( $object->in_storage )
380             ? []
381 32 100 66     238 : $c->stash->{create_requires} || $self->create_requires
382             };
383              
384 82 100       419 my %allows_map = map { ( ref $_ ) ? %{$_} : ( $_ => 1 ) } (
  17         94  
385             keys %requires_map,
386 32         107 @{ ( $object->in_storage )
387             ? ( $c->stash->{update_allows} || $self->update_allows )
388 32 100 66     161 : ( $c->stash->{create_allows} || $self->create_allows )
      33        
389             }
390             );
391              
392 32         121 foreach my $key ( keys %allows_map ) {
393              
394             # check value defined if key required
395 76         139 my $allowed_fields = $allows_map{$key};
396              
397 76 100       167 if ( ref $allowed_fields ) {
398 17         111 my $related_source = $object->result_source->related_source($key);
399 17         2621 my $related_params = $params->{$key};
400 17         53 my %allowed_related_map = map { $_ => 1 } @$allowed_fields;
  17         72  
401             my $allowed_related_cols =
402 17 50       82 ( $allowed_related_map{'*'} )
403             ? [ $related_source->columns ]
404             : $allowed_fields;
405              
406 17 50 66     359 if (ref($related_params) && reftype($related_params) eq 'ARRAY') {
407 0         0 my @related_data;
408 0         0 for my $related_param (@$related_params) {
409 0         0 my %data;
410 0         0 foreach my $related_col ( @{$allowed_related_cols} ) {
  0         0  
411 0 0       0 if (defined(
412             my $related_col_value =
413             $related_param->{$related_col}
414             )
415             ) {
416 0         0 $data{$related_col} = $related_col_value;
417             }
418             }
419 0         0 push @related_data, \%data;
420             }
421 0         0 $values{$key} = \@related_data;
422             }
423             else {
424 17         35 foreach my $related_col ( @{$allowed_related_cols} ) {
  17         45  
425 68 100       175 if (defined(
426             my $related_col_value =
427             $related_params->{$related_col}
428             )
429             ) {
430 2         12 $values{$key}{$related_col} = $related_col_value;
431             }
432             }
433             }
434             }
435             else {
436 59         136 my $value = $params->{$key};
437              
438 59 100       150 if ( $requires_map{$key} ) {
439 15 100       60 unless ( defined($value) ) {
440              
441             # if not defined look for default
442             $value = $object->result_source->column_info($key)
443 4         19 ->{default_value};
444 4 100       182 unless ( defined $value ) {
445 2         43 die "No value supplied for ${key} and no default";
446             }
447             }
448             }
449              
450             # check for multiple values
451 57 50 33     162 if ( ref($value) && !( reftype($value) eq reftype(JSON::MaybeXS::true) ) )
452             {
453 0         0 require Data::Dumper;
454 0         0 die
455 0         0 "Multiple values for '${key}': ${\Data::Dumper::Dumper($value)}";
456             }
457              
458             # check exists so we don't just end up with hash of undefs
459             # check defined to account for default values being used
460             $values{$key} = $value
461 57 100 100     266 if exists $params->{$key} || defined $value;
462             }
463             }
464              
465 30 100 66     180 unless ( keys %values || !$object->in_storage ) {
466 3         58 die 'No valid keys passed';
467             }
468              
469 27         131 return \%values;
470             }
471              
472              
473             sub delete {
474 4     4 1 41 my ( $self, $c ) = @_;
475              
476 4 50       16 if ( $c->req->has_objects ) {
477             $self->transact_objects( $c,
478 4     4   49 sub { $self->delete_objects( $c, @_ ) } );
  4         1735  
479 4         55732 $c->req->clear_objects;
480             }
481             else {
482 0         0 $c->log->error($_);
483 0         0 $self->push_error( $c,
484             { message => 'No objects on which to operate' } );
485 0         0 $c->detach();
486             }
487             }
488              
489              
490             sub save_objects {
491 19     19 1 73 my ( $self, $c, $objects ) = @_;
492              
493 19         66 foreach my $obj (@$objects) {
494 25         159 $self->save_object( $c, $obj );
495             }
496             }
497              
498              
499             sub save_object {
500 25     25 1 80 my ( $self, $c, $obj ) = @_;
501              
502 25         69 my ( $object, $params ) = @$obj;
503              
504 25 100       118 if ( $object->in_storage ) {
505 14         61 $self->update_object_from_params( $c, $object, $params );
506             }
507             else {
508 11         60 $self->insert_object_from_params( $c, $object, $params );
509             }
510              
511             }
512              
513              
514             sub update_object_from_params {
515 14     14 1 38 my ( $self, $c, $object, $params ) = @_;
516              
517 14         40 $params = {%$params, %{$object->ident_condition}};
  14         184  
518              
519 14         1204 my $updated_object =
520             DBIx::Class::ResultSet::RecursiveUpdate::Functions::recursive_update(
521             resultset => $c->req->current_result_set,
522             # unknown_params_ok => 1,
523             updates => $params,
524             );
525              
526             # replace request object with updated one for response
527 13         71134 my $vals = $c->req->get_object(0)->[1];
528 13         51 $c->req->clear_objects;
529 13         119 $c->req->add_object( [ $updated_object, $vals ] );
530             }
531              
532              
533             sub insert_object_from_params {
534              
535             #my ($self, $c, $object, $params) = @_;
536 11     11 1 34 my ( $self, undef, $object, $params ) = @_;
537              
538 11         22 my %rels;
539 11         21 while ( my ( $key, $value ) = each %{$params} ) {
  28         1722  
540 17 50 33     133 if ( ref($value) && !( reftype($value) eq reftype(JSON::MaybeXS::true) ) ) {
    50          
541 0         0 $rels{$key} = $value;
542             }
543              
544             # accessor = colname
545             elsif ( $object->can($key) ) {
546 17         436 $object->$key($value);
547             }
548              
549             # accessor != colname
550             else {
551             my $accessor =
552 0         0 $object->result_source->column_info($key)->{accessor};
553 0         0 $object->$accessor($value);
554             }
555             }
556              
557 11         83 $object->insert;
558              
559 9         20544 while ( my ( $k, $v ) = each %rels ) {
560 0 0       0 if (reftype($v) eq 'ARRAY') {
561 0         0 foreach my $next_v ( @$v ) {
562 0         0 $object->create_related($k, $next_v);
563             }
564             }
565             else {
566 0         0 $object->create_related($k => $v);
567             }
568             }
569             }
570              
571              
572             sub delete_objects {
573 4     4 1 18 my ( $self, $c, $objects ) = @_;
574              
575 4         12 map { $self->delete_object( $c, $_->[0] ) } @$objects;
  8         3693  
576             }
577              
578              
579             sub delete_object {
580              
581             #my ($self, $c, $object) = @_;
582 8     8 1 24 my ( $self, undef, $object ) = @_;
583              
584 8         65 $object->delete;
585             }
586              
587              
588             sub end : Private {
589 106     106 1 419329 my ( $self, $c ) = @_;
590              
591             # don't change the http status code if already set elsewhere
592 106 100 66     559 unless ( $c->res->status && $c->res->status != 200 ) {
593 105 100       31078 if ( $self->has_errors($c) ) {
594 33         2368 $c->res->status(400);
595             }
596             else {
597 72         5388 $c->res->status(200);
598             }
599             }
600              
601 106 100       13590 if ( $c->res->status == 200 ) {
602             $c->stash->{ $self->stash_key }->{success} =
603 72 100       16004 $self->use_json_boolean ? JSON::MaybeXS::true : 'true';
604 72 100 100     3069 if ( $self->return_object
      66        
605             && $c->req->has_objects
606             && ! exists $c->stash->{ $self->stash_key }->{ $self->data_root } ) {
607 6         19 my $returned_objects = [];
608             push( @$returned_objects, $self->each_object_inflate( $c, $_ ) )
609 6         24 for map { $_->[0] } $c->req->all_objects;
  7         64  
610 6 100       174 $c->stash->{ $self->stash_key }->{ $self->data_root } =
611             scalar(@$returned_objects) > 1
612             ? $returned_objects
613             : $returned_objects->[0];
614             }
615             }
616             else {
617             $c->stash->{ $self->stash_key }->{success} =
618 34 100       5462 $self->use_json_boolean ? JSON::MaybeXS::false : 'false';
619 34 100       161 $c->stash->{ $self->stash_key }->{messages} = $self->get_errors($c)
620             if $self->has_errors($c);
621              
622             # don't return data for error responses
623 34         204 delete $c->stash->{ $self->stash_key }->{ $self->data_root };
624             }
625              
626 106         525 $c->forward('serialize');
627 16     16   59193 }
  16         58  
  16         99  
628              
629              
630             sub each_object_inflate {
631              
632             #my ($self, $c, $object) = @_;
633 10     10 1 53 my ( $self, undef, $object ) = @_;
634              
635 10         97 return { $object->get_columns };
636             }
637              
638              
639             # from Catalyst::Action::Serialize
640 16     16 1 17416 sub serialize : ActionClass('Serialize') { }
  16     106   47  
  16         92  
641              
642              
643             sub push_error {
644 33     33 1 131 my ( $self, $c, $params ) = @_;
645 33 50       157 die 'Catalyst app object missing'
646             unless defined $c;
647 33         99 my $error = 'unknown error';
648 33 50       145 if ( exists $params->{message} ) {
649 33         128 $error = $params->{message};
650              
651             # remove newline from die "error message\n" which is required to not
652             # have the filename and line number in the error text
653 33         231 $error =~ s/\n$//;
654             }
655 33         45864 push( @{ $c->stash->{_dbic_crud_errors} }, $error );
  33         172  
656             }
657              
658              
659             sub get_errors {
660 33     33 1 2440 my ( $self, $c ) = @_;
661 33 50       145 die 'Catalyst app object missing'
662             unless defined $c;
663 33         122 return $c->stash->{_dbic_crud_errors};
664             }
665              
666              
667             sub has_errors {
668 139     139 1 456 my ( $self, $c ) = @_;
669 139 50       473 die 'Catalyst app object missing'
670             unless defined $c;
671 139         526 return exists $c->stash->{_dbic_crud_errors};
672             }
673              
674              
675             1;
676              
677             __END__
678              
679             =pod
680              
681             =head1 NAME
682              
683             Catalyst::Controller::DBIC::API - Provides a DBIx::Class web service automagically
684              
685             =head1 VERSION
686              
687             version 2.008001
688              
689             =head1 SYNOPSIS
690              
691             package MyApp::Controller::API::RPC::Artist;
692             use Moose;
693             BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC' }
694              
695             __PACKAGE__->config
696             ( # define parent chain action and PathPart
697             action => {
698             setup => {
699             Chained => '/api/rpc/rpc_base',
700             PathPart => 'artist',
701             }
702             },
703             class => 'MyAppDB::Artist',
704             resultset_class => 'MyAppDB::ResultSet::Artist',
705             create_requires => ['name', 'age'],
706             create_allows => ['nickname'],
707             update_allows => ['name', 'age', 'nickname'],
708             update_allows => ['name', 'age', 'nickname'],
709             select => ['name', 'age'],
710             prefetch => ['cds'],
711             prefetch_allows => [
712             'cds',
713             { cds => 'tracks' },
714             { cds => ['tracks'] },
715             ],
716             ordered_by => ['age'],
717             search_exposes => ['age', 'nickname', { cds => ['title', 'year'] }],
718             data_root => 'list',
719             item_root => 'data',
720             use_json_boolean => 1,
721             return_object => 1,
722             );
723              
724             # Provides the following functional endpoints:
725             # /api/rpc/artist/create
726             # /api/rpc/artist/list
727             # /api/rpc/artist/id/[id]/delete
728             # /api/rpc/artist/id/[id]/update
729              
730             =head1 DESCRIPTION
731              
732             Easily provide common API endpoints based on your L<DBIx::Class> schema classes.
733             Module provides both RPC and REST interfaces to base functionality.
734             Uses L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize> to
735             serialize response and/or deserialise request.
736              
737             =head1 OVERVIEW
738              
739             This document describes base functionlity such as list, create, delete, update
740             and the setting of config attributes. L<Catalyst::Controller::DBIC::API::RPC>
741             and L<Catalyst::Controller::DBIC::API::REST> describe details of provided
742             endpoints to those base methods.
743              
744             You will need to create a controller for each schema class you require API
745             endpoints for. For example if your schema has Artist and Track, and you want to
746             provide a RESTful interface to these, you should create
747             MyApp::Controller::API::REST::Artist and MyApp::Controller::API::REST::Track
748             which both subclass L<Catalyst::Controller::DBIC::API::REST>.
749             Similarly if you wanted to provide an RPC style interface then subclass
750             L<Catalyst::Controller::DBIC::API::RPC>. You then configure these individually
751             as specified in L</CONFIGURATION>.
752              
753             Also note that the test suite of this module has an example application used to
754             run tests against. It maybe helpful to look at that until a better tutorial is
755             written.
756              
757             =head2 CONFIGURATION
758              
759             Each of your controller classes needs to be configured to point at the relevant
760             schema class, specify what can be updated and so on, as shown in the L</SYNOPSIS>.
761              
762             The class, create_requires, create_allows and update_requires parameters can
763             also be set in the stash like so:
764              
765             sub setup :Chained('/api/rpc/rpc_base') :CaptureArgs(1) :PathPart('any') {
766             my ($self, $c, $object_type) = @_;
767              
768             if ($object_type eq 'artist') {
769             $c->stash->{class} = 'MyAppDB::Artist';
770             $c->stash->{create_requires} = [qw/name/];
771             $c->stash->{update_allows} = [qw/name/];
772             } else {
773             $self->push_error($c, { message => "invalid object_type" });
774             return;
775             }
776              
777             $self->next::method($c);
778             }
779              
780             Generally it's better to have one controller for each DBIC source with the
781             config hardcoded, but in some cases this isn't possible.
782              
783             Note that the Chained, CaptureArgs and PathPart are just standard Catalyst
784             configuration parameters and that then endpoint specified in Chained - in this
785             case '/api/rpc/rpc_base' - must actually exist elsewhere in your application.
786             See L<Catalyst::DispatchType::Chained> for more details.
787              
788             Below are explanations for various configuration parameters. Please see
789             L<Catalyst::Controller::DBIC::API::StaticArguments> for more details.
790              
791             =head3 class
792              
793             Whatever you would pass to $c->model to get a resultset for this class.
794             MyAppDB::Track for example.
795              
796             =head3 resultset_class
797              
798             Desired resultset class after accessing your model. MyAppDB::ResultSet::Track
799             for example. By default, it's DBIx::Class::ResultClass::HashRefInflator.
800             Set to empty string to leave resultset class without change.
801              
802             =head3 stash_key
803              
804             Controls where in stash request_data should be stored, and defaults to 'response'.
805              
806             =head3 data_root
807              
808             By default, the response data of multiple item actions is serialized into
809             $c->stash->{$self->stash_key}->{$self->data_root} and data_root defaults to
810             'list' to preserve backwards compatibility. This is now configuable to meet
811             the needs of the consuming client.
812              
813             =head3 item_root
814              
815             By default, the response data of single item actions is serialized into
816             $c->stash->{$self->stash_key}->{$self->item_root} and item_root default to
817             'data'.
818              
819             =head3 use_json_boolean
820              
821             By default, the response success status is set to a string value of "true" or
822             "false". If this attribute is true, JSON::MaybeXS's true() and false() will be
823             used instead. Note, this does not effect other internal processing of boolean
824             values.
825              
826             =head3 count_arg, page_arg, select_arg, search_arg, grouped_by_arg, ordered_by_arg, prefetch_arg, as_arg, total_entries_arg
827              
828             These attributes allow customization of the component to understand requests
829             made by clients where these argument names are not flexible and cannot conform
830             to this components defaults.
831              
832             =head3 create_requires
833              
834             Arrayref listing columns required to be passed to create in order for the
835             request to be valid.
836              
837             =head3 create_allows
838              
839             Arrayref listing columns additional to those specified in create_requires that
840             are not required to create but which create does allow. Columns passed to create
841             that are not listed in create_allows or create_requires will be ignored.
842              
843             =head3 update_allows
844              
845             Arrayref listing columns that update will allow. Columns passed to update that
846             are not listed here will be ignored.
847              
848             =head3 select
849              
850             Arguments to pass to L<DBIx::Class::ResultSet/select> when performing search for
851             L</list>.
852              
853             =head3 as
854              
855             Complements arguments passed to L<DBIx::Class::ResultSet/select> when performing
856             a search. This allows you to specify column names in the result for RDBMS
857             functions, etc.
858              
859             =head3 select_exposes
860              
861             Columns and related columns that are okay to return in the resultset since
862             clients can request more or less information specified than the above select
863             argument.
864              
865             =head3 prefetch
866              
867             Arguments to pass to L<DBIx::Class::ResultSet/prefetch> when performing search
868             for L</list>.
869              
870             =head3 prefetch_allows
871              
872             Arrayref listing relationships that are allowed to be prefetched.
873             This is necessary to avoid denial of service attacks in form of
874             queries which would return a large number of data
875             and unwanted disclosure of data.
876              
877             =head3 grouped_by
878              
879             Arguments to pass to L<DBIx::Class::ResultSet/group_by> when performing search
880             for L</list>.
881              
882             =head3 ordered_by
883              
884             Arguments to pass to L<DBIx::Class::ResultSet/order_by> when performing search
885             for L</list>.
886              
887             =head3 search_exposes
888              
889             Columns and related columns that are okay to search on. For example if only the
890             position column and all cd columns were to be allowed
891              
892             search_exposes => [qw/position/, { cd => ['*'] }]
893              
894             You can also use this to allow custom columns should you wish to allow them
895             through in order to be caught by a custom resultset. For example:
896              
897             package RestTest::Controller::API::RPC::TrackExposed;
898              
899             ...
900              
901             __PACKAGE__->config
902             ( ...,
903             search_exposes => [qw/position title custom_column/],
904             );
905              
906             and then in your custom resultset:
907              
908             package RestTest::Schema::ResultSet::Track;
909              
910             use base 'RestTest::Schema::ResultSet';
911              
912             sub search {
913             my $self = shift;
914             my ($clause, $params) = @_;
915              
916             # test custom attrs
917             if (my $pretend = delete $clause->{custom_column}) {
918             $clause->{'cd.year'} = $pretend;
919             }
920             my $rs = $self->SUPER::search(@_);
921             }
922              
923             =head3 count
924              
925             Arguments to pass to L<DBIx::Class::ResultSet/rows> when performing search for
926             L</list>.
927              
928             =head3 page
929              
930             Arguments to pass to L<DBIx::Class::ResultSet/page> when performing search for
931             L</list>.
932              
933             =head1 PROTECTED_METHODS
934              
935             =head2 setup
936              
937             :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('specify.in.subclass.config')
938              
939             This action is the chain root of the controller. It must either be overridden or
940             configured to provide a base PathPart to the action and also a parent action.
941             For example, for class MyAppDB::Track you might have
942              
943             package MyApp::Controller::API::RPC::Track;
944             use Moose;
945             BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC'; }
946              
947             __PACKAGE__->config
948             ( action => { setup => { PathPart => 'track', Chained => '/api/rpc/rpc_base' } },
949             ...
950             );
951              
952             # or
953              
954             sub setup :Chained('/api/rpc/rpc_base') :CaptureArgs(0) :PathPart('track') {
955             my ($self, $c) = @_;
956              
957             $self->next::method($c);
958             }
959              
960             This action does nothing by default.
961              
962             =head2 deserialize
963              
964             :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('Deserialize')
965              
966             Absorbs the request data and transforms it into useful bits by using
967             CGI::Expand->expand_hash and a smattering of JSON->decode for a handful of
968             arguments.
969              
970             Current only the following arguments are capable of being expressed as JSON:
971              
972             search_arg
973             count_arg
974             page_arg
975             ordered_by_arg
976             grouped_by_arg
977             prefetch_arg
978              
979             It should be noted that arguments can used mixed modes in with some caveats.
980             Each top level arg can be expressed as CGI::Expand with their immediate child
981             keys expressed as JSON when sending the data application/x-www-form-urlencoded.
982             Otherwise, you can send content as raw json and it will be deserialized as is
983             with no CGI::Expand expasion.
984              
985             =head2 generate_rs
986              
987             generate_rs is used by inflate_request to get a resultset for the current
988             request. It receives $c as its only argument.
989             By default it returns a resultset of the controller's class.
990             Override this method if you need to manipulate the default implementation of
991             getting a resultset.
992              
993             =head2 inflate_request
994              
995             inflate_request is called at the end of deserialize to populate key portions of
996             the request with the useful bits.
997              
998             =head2 object_with_id
999              
1000             :Chained('deserialize') :CaptureArgs(1) :PathPart('')
1001              
1002             This action is the chain root for all object level actions (such as delete and
1003             update) that operate on a single identifer. The provided identifier will be used
1004             to find that particular object and add it to the request's store ofobjects.
1005              
1006             Please see L<Catalyst::Controller::DBIC::API::Request::Context> for more
1007             details on the stored objects.
1008              
1009             =head2 objects_no_id
1010              
1011             :Chained('deserialize') :CaptureArgs(0) :PathPart('')
1012              
1013             This action is the chain root for object level actions (such as create, update,
1014             or delete) that can involve more than one object. The data stored at the
1015             data_root of the request_data will be interpreted as an array of hashes on which
1016             to operate. If the hashes are missing an 'id' key, they will be considered a
1017             new object to be created. Otherwise, the values in the hash will be used to
1018             perform an update. As a special case, a single hash sent will be coerced into
1019             an array.
1020              
1021             Please see L<Catalyst::Controller::DBIC::API::Request::Context> for more
1022             details on the stored objects.
1023              
1024             =head2 object_lookup
1025              
1026             This method provides the look up functionality for an object based on 'id'.
1027             It is passed the current $c and the id to be used to perform the lookup.
1028             Dies if there is no provided id or if no object was found.
1029              
1030             =head2 list
1031              
1032             list's steps are broken up into three distinct methods:
1033              
1034             =over
1035              
1036             =item L</list_munge_parameters>
1037              
1038             =item L</list_perform_search>
1039              
1040             =item L</list_format_output>.
1041              
1042             =back
1043              
1044             The goal of this method is to call ->search() on the current_result_set,
1045             change the resultset class of the result (if needed), and return it in
1046             $c->stash->{$self->stash_key}->{$self->data_root}.
1047              
1048             Please see the individual methods for more details on what actual processing
1049             takes place.
1050              
1051             If the L</select> config param is defined then the hashes will contain only
1052             those columns, otherwise all columns in the object will be returned.
1053             L</select> of course supports the function/procedure calling semantics that
1054             L<DBIx::Class::ResultSet/select> supports.
1055              
1056             In order to have proper column names in the result, provide arguments in L</as>
1057             (which also follows L<DBIx::Class::ResultSet/as> semantics.
1058             Similarly L</count>, L</page>, L</grouped_by> and L</ordered_by> affect the
1059             maximum number of rows returned as well as the ordering and grouping.
1060              
1061             Note that if select, count, ordered_by or grouped_by request parameters are
1062             present, these will override the values set on the class with select becoming
1063             bound by the select_exposes attribute.
1064              
1065             If not all objects in the resultset are required then it's possible to pass
1066             conditions to the method as request parameters. You can use a JSON string as
1067             the 'search' parameter for maximum flexibility or use L<CGI::Expand> syntax.
1068             In the second case the request parameters are expanded into a structure and
1069             then used as the search condition.
1070              
1071             For example, these request parameters:
1072              
1073             ?search.name=fred&search.cd.artist=luke
1074             OR
1075             ?search={"name":"fred","cd": {"artist":"luke"}}
1076              
1077             Would result in this search (where 'name' is a column of the result class, 'cd'
1078             is a relation of the result class and 'artist' is a column of the related class):
1079              
1080             $rs->search({ name => 'fred', 'cd.artist' => 'luke' }, { join => ['cd'] })
1081              
1082             It is also possible to use a JSON string for expandeded parameters:
1083              
1084             ?search.datetime={"-between":["2010-01-06 19:28:00","2010-01-07 19:28:00"]}
1085              
1086             Note that if pagination is needed, this can be achieved using a combination of
1087             the L</count> and L</page> parameters. For example:
1088              
1089             ?page=2&count=20
1090              
1091             Would result in this search:
1092              
1093             $rs->search({}, { page => 2, rows => 20 })
1094              
1095             =head2 list_munge_parameters
1096              
1097             list_munge_parameters is a noop by default. All arguments will be passed through
1098             without any manipulation. In order to successfully manipulate the parameters
1099             before the search is performed, simply access
1100             $c->req->search_parameters|search_attributes (ArrayRef and HashRef respectively),
1101             which correspond directly to ->search($parameters, $attributes).
1102             Parameter keys will be in already-aliased form.
1103             To store the munged parameters call $c->req->_set_search_parameters($newparams)
1104             and $c->req->_set_search_attributes($newattrs).
1105              
1106             =head2 list_perform_search
1107              
1108             list_perform_search executes the actual search. current_result_set is updated to
1109             contain the result returned from ->search. If paging was requested,
1110             search_total_entries will be set as well.
1111              
1112             =head2 list_format_output
1113              
1114             list_format_output prepares the response for transmission across the wire.
1115             A copy of the current_result_set is taken and its result_class is set to
1116             L<DBIx::Class::ResultClass::HashRefInflator>. Each row in the resultset is then
1117             iterated and passed to L</row_format_output> with the result of that call added
1118             to the output.
1119              
1120             =head2 row_format_output
1121              
1122             row_format_output is called each row of the inflated output generated from the
1123             search. It receives two arguments, the catalyst context and the hashref that
1124             represents the row. By default, this method is merely a passthrough.
1125              
1126             =head2 item
1127              
1128             item will return a single object called by identifier in the uri. It will be
1129             inflated via each_object_inflate.
1130              
1131             =head2 update_or_create
1132              
1133             update_or_create is responsible for iterating any stored objects and performing
1134             updates or creates. Each object is first validated to ensure it meets the
1135             criteria specified in the L</create_requires> and L</create_allows> (or
1136             L</update_allows>) parameters of the controller config. The objects are then
1137             committed within a transaction via L</transact_objects> using a closure around
1138             L</save_objects>.
1139              
1140             =head2 transact_objects
1141              
1142             transact_objects performs the actual commit to the database via $schema->txn_do.
1143             This method accepts two arguments, the context and a coderef to be used within
1144             the transaction. All of the stored objects are passed as an arrayref for the
1145             only argument to the coderef.
1146              
1147             =head2 validate_objects
1148              
1149             This is a shortcut method for performing validation on all of the stored objects
1150             in the request. Each object's provided values (for create or update) are updated
1151             to the allowed values permitted by the various config parameters.
1152              
1153             =head2 validate_object
1154              
1155             validate_object takes the context and the object as an argument. It then filters
1156             the passed values in slot two of the tuple through the create|update_allows
1157             configured. It then returns those filtered values. Values that are not allowed
1158             are silently ignored. If there are no values for a particular key, no valid
1159             values at all, or multiple of the same key, this method will die.
1160              
1161             =head2 delete
1162              
1163             delete operates on the stored objects in the request. It first transacts the
1164             objects, deleting them in the database using L</transact_objects> and a closure
1165             around L</delete_objects>, and then clears the request store of objects.
1166              
1167             =head2 save_objects
1168              
1169             This method is used by update_or_create to perform the actual database
1170             manipulations. It iterates each object calling L</save_object>.
1171              
1172             =head2 save_object
1173              
1174             save_object first checks to see if the object is already in storage. If so, it
1175             calls L</update_object_from_params> otherwise L</insert_object_from_params>.
1176              
1177             =head2 update_object_from_params
1178              
1179             update_object_from_params iterates through the params to see if any of them are
1180             pertinent to relations. If so it calls L</update_object_relation> with the
1181             object, and the relation parameters. Then it calls ->update on the object.
1182              
1183             =head2 insert_object_from_params
1184              
1185             Sets the columns of the object, then calls ->insert.
1186              
1187             =head2 delete_objects
1188              
1189             Iterates through each object calling L</delete_object>.
1190              
1191             =head2 delete_object
1192              
1193             Performs the actual ->delete on the object.
1194              
1195             =head2 end
1196              
1197             end performs the final manipulation of the response before it is serialized.
1198             This includes setting the success of the request both at the HTTP layer and
1199             JSON layer. If configured with return_object true, and there are stored objects
1200             as the result of create or update, those will be inflated according to the
1201             schema and get_inflated_columns
1202              
1203             =head2 each_object_inflate
1204              
1205             each_object_inflate executes during L</end> and allows hooking into the process
1206             of inflating the objects to return in the response. Receives, the context, and
1207             the object as arguments.
1208              
1209             This only executes if L</return_object> if set and if there are any objects to
1210             actually return.
1211              
1212             =head2 serialize
1213              
1214             multiple actions forward to serialize which uses Catalyst::Action::Serialize.
1215              
1216             =head2 push_error
1217              
1218             Stores an error message into the stash to be later retrieved by L</end>.
1219             Accepts a Dict[message => Str] parameter that defines the error message.
1220              
1221             =head2 get_errors
1222              
1223             Returns all of the errors stored in the stash.
1224              
1225             =head2 has_errors
1226              
1227             Returns true if errors are stored in the stash.
1228              
1229             =head1 PRIVATE_METHODS
1230              
1231             =head2 begin
1232              
1233             :Private
1234              
1235             begin is provided in the base class to setup the Catalyst request object by
1236             applying the DBIC::API::Request role.
1237              
1238             =head1 EXTENDING
1239              
1240             By default the create, delete and update actions will not return anything apart
1241             from the success parameter set in L</end>, often this is not ideal but the
1242             required behaviour varies from application to application. So normally it's
1243             sensible to write an intermediate class which your main controller classes
1244             subclass from.
1245              
1246             For example if you wanted create to return the JSON for the newly created
1247             object you might have something like:
1248              
1249             package MyApp::ControllerBase::DBIC::API::RPC;
1250             ...
1251             use Moose;
1252             BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC' };
1253             ...
1254             sub create :Chained('setup') :Args(0) :PathPart('create') {
1255             my ($self, $c) = @_;
1256              
1257             # $c->req->all_objects will contain all of the created
1258             $self->next::method($c);
1259              
1260             if ($c->req->has_objects) {
1261             # $c->stash->{$self->stash_key} will be serialized in the end action
1262             $c->stash->{$self->stash_key}->{$self->data_root} = [ map { { $_->get_inflated_columns } } ($c->req->all_objects) ] ;
1263             }
1264             }
1265              
1266             package MyApp::Controller::API::RPC::Track;
1267             ...
1268             use Moose;
1269             BEGIN { extends 'MyApp::ControllerBase::DBIC::API::RPC' };
1270             ...
1271              
1272             It should be noted that the return_object attribute will produce the above
1273             result for you, free of charge.
1274              
1275             Similarly you might want create, update and delete to all forward to the list
1276             action once they are done so you can refresh your view. This should also be
1277             simple enough.
1278              
1279             If more extensive customization is required, it is recommened to peer into the
1280             roles that comprise the system and make use
1281              
1282             =head1 NOTES
1283              
1284             It should be noted that version 1.004 and above makes a rapid depature from the
1285             status quo. The internals were revamped to use more modern tools such as Moose
1286             and its role system to refactor functionality out into self-contained roles.
1287              
1288             To this end, internally, this module now understands JSON boolean values (as
1289             represented by the JSON::MaybeXS module) and will Do The Right Thing in
1290             handling those values. This means you can have ColumnInflators installed that
1291             can covert between JSON booleans and whatever your database wants for boolean
1292             values.
1293              
1294             Validation for various *_allows or *_exposes is now accomplished via
1295             Data::DPath::Validator with a lightly simplified, via a subclass of
1296             Data::DPath::Validator::Visitor.
1297              
1298             The rough jist of the process goes as follows: Arguments provided to those
1299             attributes are fed into the Validator and Data::DPaths are generated.
1300             Then incoming requests are validated against these paths generated.
1301             The validator is set in "loose" mode meaning only one path is required to match.
1302             For more information, please see L<Data::DPath::Validator> and more specifically
1303             L<Catalyst::Controller::DBIC::API::Validator>.
1304              
1305             Since 2.001:
1306             Transactions are used. The stash is put aside in favor of roles applied to the
1307             request object with additional accessors.
1308             Error handling is now much more consistent with most errors immediately detaching.
1309             The internals are much easier to read and understand with lots more documentation.
1310              
1311             Since 2.006:
1312             The SQL::Abstract -and, -not and -or operators are supported.
1313              
1314             =head1 AUTHORS
1315              
1316             =over 4
1317              
1318             =item *
1319              
1320             Nicholas Perez <nperez@cpan.org>
1321              
1322             =item *
1323              
1324             Luke Saunders <luke.saunders@gmail.com>
1325              
1326             =item *
1327              
1328             Alexander Hartmaier <abraxxa@cpan.org>
1329              
1330             =item *
1331              
1332             Florian Ragwitz <rafl@debian.org>
1333              
1334             =item *
1335              
1336             Oleg Kostyuk <cub.uanic@gmail.com>
1337              
1338             =item *
1339              
1340             Samuel Kaufman <sam@socialflow.com>
1341              
1342             =back
1343              
1344             =head1 COPYRIGHT AND LICENSE
1345              
1346             This software is copyright (c) 2019 by Luke Saunders, Nicholas Perez, Alexander Hartmaier, et al.
1347              
1348             This is free software; you can redistribute it and/or modify it under
1349             the same terms as the Perl 5 programming language system itself.
1350              
1351             =cut