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