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