fetch a model instance on every request instead of using stored_result_source for...
[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 get a resultset for the current
195 request. It receives $c as its only argument.
196 By default it returns a resultset of the controller's class.
197 Override this method if you need to manipulate the default implementation of
198 getting a resultset.
199
200 =cut
201
202 sub generate_rs
203 {
204     my ($self, $c) = @_;
205
206     return $c->model($self->class);
207 }
208
209 =method_protected inflate_request
210
211 inflate_request is called at the end of deserialize to populate key portions of the request with the useful bits
212
213 =cut
214
215 sub inflate_request
216 {
217     my ($self, $c, $params) = @_;
218
219     try
220     {
221         # set static arguments
222         $c->req->_set_controller($self);
223
224         # set request arguments
225         $c->req->_set_request_data($params);
226
227         # set the current resultset
228         $c->req->_set_current_result_set($self->generate_rs($c));
229
230     }
231     catch
232     {
233         $c->log->error($_);
234         $self->push_error($c, { message => $_ });
235         $c->detach();
236     }
237 }
238
239 =method_protected object_with_id
240
241  :Chained('deserialize') :CaptureArgs(1) :PathPart('')
242
243 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.
244
245 =cut
246
247 sub object_with_id :Chained('deserialize') :CaptureArgs(1) :PathPart('')
248 {
249         my ($self, $c, $id) = @_;
250
251     my $vals = $c->req->request_data->{$self->data_root};
252     unless(defined($vals))
253     {
254         # no data root, assume the request_data itself is the payload
255         $vals = $c->req->request_data;
256     }
257
258     try
259     {
260         # there can be only one set of data
261         $c->req->add_object([$self->object_lookup($c, $id), $vals]);
262     }
263     catch
264     {
265         $c->log->error($_);
266         $self->push_error($c, { message => $_ });
267         $c->detach();
268     }
269 }
270
271 =method_protected objects_no_id
272
273  :Chained('deserialize') :CaptureArgs(0) :PathPart('')
274
275 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.
276
277 =cut
278
279 sub objects_no_id :Chained('deserialize') :CaptureArgs(0) :PathPart('')
280 {
281     my ($self, $c) = @_;
282
283     if($c->req->has_request_data)
284     {
285         my $data = $c->req->request_data;
286         my $vals;
287
288         if(exists($data->{$self->data_root}) && defined($data->{$self->data_root}))
289         {
290             my $root = $data->{$self->data_root};
291             if(reftype($root) eq 'ARRAY')
292             {
293                 $vals = $root;
294             }
295             elsif(reftype($root) eq 'HASH')
296             {
297                 $vals = [$root];
298             }
299             else
300             {
301                 $c->log->error('Invalid request data');
302                 $self->push_error($c, { message => 'Invalid request data' });
303                 $c->detach();
304             }
305         }
306         else
307         {
308             # no data root, assume the request_data itself is the payload
309             $vals = [$c->req->request_data];
310         }
311
312         foreach my $val (@$vals)
313         {
314             unless(exists($val->{id}))
315             {
316                 $c->req->add_object([$c->req->current_result_set->new_result({}), $val]);
317                 next;
318             }
319
320             try
321             {
322                 $c->req->add_object([$self->object_lookup($c, $val->{id}), $val]);
323             }
324             catch
325             {
326                 $c->log->error($_);
327                 $self->push_error($c, { message => $_ });
328                 $c->detach();
329             }
330         }
331     }
332 }
333
334 =method_protected object_lookup
335
336 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.
337
338 =cut
339
340 sub object_lookup
341 {
342     my ($self, $c, $id) = @_;
343
344     die 'No valid ID provided for look up' unless defined $id and length $id;
345     my $object = $c->req->current_result_set->find($id);
346     die "No object found for id '$id'" unless defined $object;
347     return $object;
348 }
349
350 =method_protected list
351
352 list's steps are broken up into three distinct methods: L</list_munge_parameters>, L</list_perform_search>, and L</list_format_output>.
353
354 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.
355
356 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.
357
358 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.
359
360 For example, these request parameters:
361
362  ?search.name=fred&search.cd.artist=luke
363  OR
364  ?search={"name":"fred","cd": {"artist":"luke"}}
365
366 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):
367
368  $rs->search({ name => 'fred', 'cd.artist' => 'luke' }, { join => ['cd'] })
369
370 It is also possible to use a JSON string for expandeded parameters:
371
372  ?search.datetime={"-between":["2010-01-06 19:28:00","2010-01-07 19:28:00"]}
373
374 Note that if pagination is needed, this can be achieved using a combination of the L</count> and L</page> parameters. For example:
375
376   ?page=2&count=20
377
378 Would result in this search:
379
380  $rs->search({}, { page => 2, rows => 20 })
381
382 =cut
383
384 sub list
385 {
386     my ($self, $c) = @_;
387
388     $self->list_munge_parameters($c);
389     $self->list_perform_search($c);
390     $self->list_format_output($c);
391
392     # make sure there are no objects lingering
393     $c->req->clear_objects();
394 }
395
396 =method_protected list_munge_parameters
397
398 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.
399 To store the munged parameters call $c->req->_set_search_parameters($newparams) and $c->req->_set_search_attributes($newattrs).
400
401 =cut
402
403 sub list_munge_parameters { } # noop by default
404
405 =method_protected list_perform_search
406
407 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.
408
409 =cut
410
411 sub list_perform_search
412 {
413     my ($self, $c) = @_;
414
415     try
416     {
417         my $req = $c->req;
418
419         my $rs = $req->current_result_set->search
420         (
421             $req->search_parameters,
422             $req->search_attributes
423         );
424
425         $req->_set_current_result_set($rs);
426
427         $req->_set_search_total_entries($req->current_result_set->pager->total_entries)
428             if $req->has_search_attributes && (exists($req->search_attributes->{page}) && defined($req->search_attributes->{page}) && length($req->search_attributes->{page}));
429     }
430     catch
431     {
432         $c->log->error($_);
433         $self->push_error($c, { message => 'a database error has occured.' });
434         $c->detach();
435     }
436 }
437
438 =method_protected list_format_output
439
440 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.
441
442 =cut
443
444 sub list_format_output
445 {
446     my ($self, $c) = @_;
447
448     my $rs = $c->req->current_result_set->search;
449     $rs->result_class($self->result_class) if $self->result_class;
450
451     try
452     {
453         my $output = {};
454         my $formatted = [];
455
456         foreach my $row ($rs->all)
457         {
458             push(@$formatted, $self->row_format_output($c, $row));
459         }
460
461         $output->{$self->data_root} = $formatted;
462
463         if ($c->req->has_search_total_entries)
464         {
465             $output->{$self->total_entries_arg} = $c->req->search_total_entries + 0;
466         }
467
468         $c->stash->{$self->stash_key} = $output;
469     }
470     catch
471     {
472         $c->log->error($_);
473         $self->push_error($c, { message => 'a database error has occured.' });
474         $c->detach();
475     }
476 }
477
478 =method_protected row_format_output
479
480 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.
481
482 =cut
483
484 sub row_format_output
485 {
486     #my ($self, $c, $row) = @_;
487     my ($self, undef, $row) = @_;
488     return $row; # passthrough by default
489 }
490
491 =method_protected item
492
493 item will return a single object called by identifier in the uri. It will be inflated via each_object_inflate.
494
495 =cut
496
497 sub item
498 {
499     my ($self, $c) = @_;
500
501     if($c->req->count_objects != 1)
502     {
503         $c->log->error($_);
504         $self->push_error($c, { message => 'No objects on which to operate' });
505         $c->detach();
506     }
507     else
508     {
509         $c->stash->{$self->stash_key}->{$self->item_root} = $self->each_object_inflate($c, $c->req->get_object(0)->[0]);
510     }
511 }
512
513 =method_protected update_or_create
514
515 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>.
516
517 =cut
518
519 sub update_or_create
520 {
521     my ($self, $c) = @_;
522
523     if($c->req->has_objects)
524     {
525         $self->validate_objects($c);
526         $self->transact_objects($c, sub { $self->save_objects($c, @_) } );
527     }
528     else
529     {
530         $c->log->error($_);
531         $self->push_error($c, { message => 'No objects on which to operate' });
532         $c->detach();
533     }
534 }
535
536 =method_protected transact_objects
537
538 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.
539
540 =cut
541
542 sub transact_objects
543 {
544     my ($self, $c, $coderef) = @_;
545
546     try
547     {
548         $self->stored_result_source->schema->txn_do
549         (
550             $coderef,
551             $c->req->objects
552         );
553     }
554     catch
555     {
556         $c->log->error($_);
557         $self->push_error($c, { message => 'a database error has occured.' });
558         $c->detach();
559     }
560 }
561
562 =method_protected validate_objects
563
564 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.
565
566 =cut
567
568 sub validate_objects
569 {
570     my ($self, $c) = @_;
571
572     try
573     {
574         foreach my $obj ($c->req->all_objects)
575         {
576             $obj->[1] = $self->validate_object($c, $obj);
577         }
578     }
579     catch
580     {
581         my $err = $_;
582         $c->log->error($err);
583         $err =~ s/\s+at\s+.+\n$//g;
584         $self->push_error($c, { message => $err });
585         $c->detach();
586     }
587 }
588
589 =method_protected validate_object
590
591 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.
592
593 =cut
594
595 sub validate_object
596 {
597     my ($self, $c, $obj) = @_;
598     my ($object, $params) = @$obj;
599
600     my %values;
601     my %requires_map = map
602     {
603         $_ => 1
604     }
605     @{
606         ($object->in_storage)
607         ? []
608         : $c->stash->{create_requires} || $self->create_requires
609     };
610
611     my %allows_map = map
612     {
613         (ref $_) ? %{$_} : ($_ => 1)
614     }
615     (
616         keys %requires_map,
617         @{
618             ($object->in_storage)
619             ? ($c->stash->{update_allows} || $self->update_allows)
620             : ($c->stash->{create_allows} || $self->create_allows)
621         }
622     );
623
624     foreach my $key (keys %allows_map)
625     {
626         # check value defined if key required
627         my $allowed_fields = $allows_map{$key};
628
629         if (ref $allowed_fields)
630         {
631             my $related_source = $object->result_source->related_source($key);
632             my $related_params = $params->{$key};
633             my %allowed_related_map = map { $_ => 1 } @$allowed_fields;
634             my $allowed_related_cols = ($allowed_related_map{'*'}) ? [$related_source->columns] : $allowed_fields;
635
636             foreach my $related_col (@{$allowed_related_cols})
637             {
638                 if (defined(my $related_col_value = $related_params->{$related_col})) {
639                     $values{$key}{$related_col} = $related_col_value;
640                 }
641             }
642         }
643         else
644         {
645             my $value = $params->{$key};
646
647             if ($requires_map{$key})
648             {
649                 unless (defined($value))
650                 {
651                     # if not defined look for default
652                     $value = $object->result_source->column_info($key)->{default_value};
653                     unless (defined $value)
654                     {
655                         die "No value supplied for ${key} and no default";
656                     }
657                 }
658             }
659
660             # check for multiple values
661             if (ref($value) && !(reftype($value) eq reftype(JSON::true)))
662             {
663                 require Data::Dumper;
664                 die "Multiple values for '${key}': ${\Data::Dumper::Dumper($value)}";
665             }
666
667             # check exists so we don't just end up with hash of undefs
668             # check defined to account for default values being used
669             $values{$key} = $value if exists $params->{$key} || defined $value;
670         }
671     }
672
673     unless (keys %values || !$object->in_storage)
674     {
675         die 'No valid keys passed';
676     }
677
678     return \%values;
679 }
680
681 =method_protected delete
682
683 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.
684
685 =cut
686
687 sub delete
688 {
689     my ($self, $c) = @_;
690
691     if($c->req->has_objects)
692     {
693         $self->transact_objects($c, sub { $self->delete_objects($c, @_) });
694         $c->req->clear_objects;
695     }
696     else
697     {
698         $c->log->error($_);
699         $self->push_error($c, { message => 'No objects on which to operate' });
700         $c->detach();
701     }
702 }
703
704 =method_protected save_objects
705
706 This method is used by update_or_create to perform the actual database manipulations. It iterates each object calling L</save_object>.
707
708 =cut
709
710 sub save_objects
711 {
712     my ($self, $c, $objects) = @_;
713
714     foreach my $obj (@$objects)
715     {
716         $self->save_object($c, $obj);
717     }
718 }
719
720 =method_protected save_object
721
722 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>
723
724 =cut
725
726 sub save_object
727 {
728     my ($self, $c, $obj) = @_;
729
730     my ($object, $params) = @$obj;
731
732     if ($object->in_storage)
733     {
734         $self->update_object_from_params($c, $object, $params);
735     }
736     else
737     {
738         $self->insert_object_from_params($c, $object, $params);
739     }
740
741 }
742
743 =method_protected update_object_from_params
744
745 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.
746
747 =cut
748
749 sub update_object_from_params
750 {
751     my ($self, $c, $object, $params) = @_;
752
753     foreach my $key (keys %$params)
754     {
755         my $value = $params->{$key};
756         if (ref($value) && !(reftype($value) eq reftype(JSON::true)))
757         {
758             $self->update_object_relation($c, $object, delete $params->{$key}, $key);
759         }
760         # accessor = colname
761         elsif ($object->can($key)) {
762             $object->$key($value);
763         }
764         # accessor != colname
765         else {
766             my $accessor = $object->result_source->column_info($key)->{accessor};
767             $object->$accessor($value);
768         }
769     }
770
771     $object->update();
772 }
773
774 =method_protected update_object_relation
775
776 update_object_relation finds the relation to the object, then calls ->update with the specified parameters
777
778 =cut
779
780 sub update_object_relation
781 {
782     my ($self, $c, $object, $related_params, $relation) = @_;
783     my $row = $object->find_related($relation, {} , {});
784
785     if ($row) {
786         foreach my $key (keys %$related_params) {
787             my $value = $related_params->{$key};
788             if (ref($value) && !(reftype($value) eq reftype(JSON::true)))
789             {
790                 $self->update_object_relation($c, $row, delete $related_params->{$key}, $key);
791             }
792             # accessor = colname
793             elsif ($row->can($key)) {
794                 $row->$key($value);
795             }
796             # accessor != colname
797             else {
798                 my $accessor = $row->result_source->column_info($key)->{accessor};
799                 $row->$accessor($value);
800             }
801         }
802         $row->update();
803     }
804     else {
805         $object->create_related($relation, $related_params);
806     }
807 }
808
809 =method_protected insert_object_from_params
810
811 insert_object_from_params sets the columns for the object, then calls ->insert
812
813 =cut
814
815 sub insert_object_from_params
816 {
817     #my ($self, $c, $object, $params) = @_;
818     my ($self, undef, $object, $params) = @_;
819
820     my %rels;
821     while (my ($k, $v) = each %{ $params }) {
822         if (ref($v) && !(reftype($v) eq reftype(JSON::true))) {
823             $rels{$k} = $v;
824         }
825         else {
826             $object->set_column($k => $v);
827         }
828     }
829
830     $object->insert;
831
832     while (my ($k, $v) = each %rels) {
833         $object->create_related($k, $v);
834     }
835 }
836
837 =method_protected delete_objects
838
839 delete_objects iterates through each object calling L</delete_object>
840
841 =cut
842
843 sub delete_objects
844 {
845     my ($self, $c, $objects) = @_;
846
847     map { $self->delete_object($c, $_->[0]) } @$objects;
848 }
849
850 =method_protected delete_object
851
852 Performs the actual ->delete on the object
853
854 =cut
855
856 sub delete_object
857 {
858     #my ($self, $c, $object) = @_;
859     my ($self, undef, $object) = @_;
860
861     $object->delete;
862 }
863
864 =method_protected end
865
866 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
867
868 =cut
869
870 sub end :Private
871 {
872     my ($self, $c) = @_;
873
874     # don't change the http status code if already set elsewhere
875     unless ($c->res->status && $c->res->status != 200) {
876         if ($self->has_errors($c)) {
877             $c->res->status(400);
878         }
879         else {
880             $c->res->status(200);
881         }
882     }
883
884     if ($c->res->status == 200) {
885         $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::true : 'true';
886         if($self->return_object && $c->req->has_objects) {
887             my $returned_objects = [];
888             push(@$returned_objects, $self->each_object_inflate($c, $_)) for map { $_->[0] } $c->req->all_objects;
889             $c->stash->{$self->stash_key}->{$self->data_root} = scalar(@$returned_objects) > 1 ? $returned_objects : $returned_objects->[0];
890         }
891     }
892     else {
893         $c->stash->{$self->stash_key}->{success} = $self->use_json_boolean ? JSON::false : 'false';
894         $c->stash->{$self->stash_key}->{messages} = $self->get_errors($c)
895             if $self->has_errors($c);
896         # don't return data for error responses
897         delete $c->stash->{$self->stash_key}->{$self->data_root};
898     }
899
900     $c->forward('serialize');
901 }
902
903 =method_protected each_object_inflate
904
905 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.
906
907 This only executes if L</return_object> if set and if there are any objects to actually return.
908
909 =cut
910
911 sub each_object_inflate
912 {
913     #my ($self, $c, $object) = @_;
914     my ($self, undef, $object) = @_;
915
916     return { $object->get_columns };
917 }
918
919 =method_protected serialize
920
921 multiple actions forward to serialize which uses Catalyst::Action::Serialize.
922
923 =cut
924
925 # from Catalyst::Action::Serialize
926 sub serialize :ActionClass('Serialize') { }
927
928 =method_protected push_error
929
930 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.
931
932 =cut
933
934 sub push_error
935 {
936     my ( $self, $c, $params ) = @_;
937     die 'Catalyst app object missing'
938         unless defined $c;
939     my $error = 'unknown error';
940     if (exists $params->{message}) {
941         $error = $params->{message};
942         # remove newline from die "error message\n" which is required to not
943         # have the filename and line number in the error text
944         $error =~ s/\n$//;
945     }
946     push( @{$c->stash->{_dbic_crud_errors}}, $error);
947 }
948
949 =method_protected get_errors
950
951 get_errors returns all of the errors stored in the stash
952
953 =cut
954
955 sub get_errors
956 {
957     my ( $self, $c ) = @_;
958     die 'Catalyst app object missing'
959         unless defined $c;
960     return $c->stash->{_dbic_crud_errors};
961 }
962
963 =method_protected has_errors
964
965 returns returns true if errors are stored in the stash
966
967 =cut
968
969 sub has_errors {
970     my ( $self, $c ) = @_;
971     die 'Catalyst app object missing'
972         unless defined $c;
973     return exists $c->stash->{_dbic_crud_errors};
974 }
975
976 =head1 DESCRIPTION
977
978 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.
979
980 =head1 OVERVIEW
981
982 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.
983
984 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>.
985
986 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.
987
988 =head2 CONFIGURATION
989
990 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>.
991
992 The class, create_requires, create_allows and update_requires parameters can also be set in the stash like so:
993
994   sub setup :Chained('/api/rpc/rpc_base') :CaptureArgs(1) :PathPart('any') {
995     my ($self, $c, $object_type) = @_;
996
997     if ($object_type eq 'artist') {
998       $c->stash->{class} = 'MyAppDB::Artist';
999       $c->stash->{create_requires} = [qw/name/];
1000       $c->stash->{update_allows} = [qw/name/];
1001     } else {
1002       $self->push_error($c, { message => "invalid object_type" });
1003       return;
1004     }
1005
1006     $self->next::method($c);
1007   }
1008
1009 Generally it's better to have one controller for each DBIC source with the config hardcoded, but in some cases this isn't possible.
1010
1011 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.
1012
1013 Below are explanations for various configuration parameters. Please see L<Catalyst::Controller::DBIC::API::StaticArguments> for more details.
1014
1015 =head3 class
1016
1017 Whatever you would pass to $c->model to get a resultset for this class. MyAppDB::Track for example.
1018
1019 =head3 resultset_class
1020
1021 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.
1022
1023 =head3 stash_key
1024
1025 Controls where in stash request_data should be stored, and defaults to 'response'.
1026
1027 =head3 data_root
1028
1029 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.
1030
1031 =head3 use_json_boolean
1032
1033 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.
1034
1035 =head3 count_arg, page_arg, select_arg, search_arg, grouped_by_arg, ordered_by_arg, prefetch_arg, as_arg, total_entries_arg
1036
1037 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.
1038
1039 =head3 create_requires
1040
1041 Arrayref listing columns required to be passed to create in order for the request to be valid.
1042
1043 =head3 create_allows
1044
1045 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.
1046
1047 =head3 update_allows
1048
1049 Arrayref listing columns that update will allow. Columns passed to update that are not listed here will be ignored.
1050
1051 =head3 select
1052
1053 Arguments to pass to L<DBIx::Class::ResultSet/select> when performing search for L</list>.
1054
1055 =head3 as
1056
1057 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.
1058
1059 =head3 select_exposes
1060
1061 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.
1062
1063 =head3 prefetch
1064
1065 Arguments to pass to L<DBIx::Class::ResultSet/prefetch> when performing search for L</list>.
1066
1067 =head3 prefetch_allows
1068
1069 Arrayref listing relationships that are allowed to be prefetched.
1070 This is necessary to avoid denial of service attacks in form of
1071 queries which would return a large number of data
1072 and unwanted disclosure of data.
1073
1074 =head3 grouped_by
1075
1076 Arguments to pass to L<DBIx::Class::ResultSet/group_by> when performing search for L</list>.
1077
1078 =head3 ordered_by
1079
1080 Arguments to pass to L<DBIx::Class::ResultSet/order_by> when performing search for L</list>.
1081
1082 =head3 search_exposes
1083
1084 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
1085
1086  search_exposes => [qw/position/, { cd => ['*'] }]
1087
1088 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:
1089
1090   package RestTest::Controller::API::RPC::TrackExposed;
1091
1092   ...
1093
1094   __PACKAGE__->config
1095     ( ...,
1096       search_exposes => [qw/position title custom_column/],
1097     );
1098
1099 and then in your custom resultset:
1100
1101   package RestTest::Schema::ResultSet::Track;
1102
1103   use base 'RestTest::Schema::ResultSet';
1104
1105   sub search {
1106     my $self = shift;
1107     my ($clause, $params) = @_;
1108
1109     # test custom attrs
1110     if (my $pretend = delete $clause->{custom_column}) {
1111       $clause->{'cd.year'} = $pretend;
1112     }
1113     my $rs = $self->SUPER::search(@_);
1114   }
1115
1116 =head3 count
1117
1118 Arguments to pass to L<DBIx::Class::ResultSet/rows> when performing search for L</list>.
1119
1120 =head3 page
1121
1122 Arguments to pass to L<DBIx::Class::ResultSet/page> when performing search for L</list>.
1123
1124 =head1 EXTENDING
1125
1126 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.
1127
1128 For example if you wanted create to return the JSON for the newly created object you might have something like:
1129
1130   package MyApp::ControllerBase::DBIC::API::RPC;
1131   ...
1132   use Moose;
1133   BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC' };
1134   ...
1135   sub create :Chained('setup') :Args(0) :PathPart('create') {
1136     my ($self, $c) = @_;
1137
1138     # $c->req->all_objects will contain all of the created
1139     $self->next::method($c);
1140
1141     if ($c->req->has_objects) {
1142       # $c->stash->{$self->stash_key} will be serialized in the end action
1143       $c->stash->{$self->stash_key}->{$self->data_root} = [ map { { $_->get_inflated_columns } } ($c->req->all_objects) ] ;
1144     }
1145   }
1146
1147   package MyApp::Controller::API::RPC::Track;
1148   ...
1149   use Moose;
1150   BEGIN { extends 'MyApp::ControllerBase::DBIC::API::RPC' };
1151   ...
1152
1153 It should be noted that the return_object attribute will produce the above result for you, free of charge.
1154
1155 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.
1156
1157 If more extensive customization is required, it is recommened to peer into the roles that comprise the system and make use
1158
1159 =head1 NOTES
1160
1161 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.
1162
1163 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.
1164
1165 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>.
1166
1167 Since 2.00100:
1168 Transactions are used. The stash is put aside in favor of roles applied to the request object with additional accessors.
1169 Error handling is now much more consistent with most errors immediately detaching.
1170 The internals are much easier to read and understand with lots more documentation.
1171
1172 =cut
1173
1174 1;