1 package Catalyst::Controller::DBIC::API;
3 #ABSTRACT: Provides a DBIx::Class web service automagically
5 BEGIN { extends 'Catalyst::Controller'; }
8 use DBIx::Class::ResultClass::HashRefInflator;
10 use Test::Deep::NoTest('eq_deeply');
11 use MooseX::Types::Moose(':all');
13 use Scalar::Util( 'blessed', 'reftype' );
15 use Catalyst::Controller::DBIC::API::Request;
16 use DBIx::Class::ResultSet::RecursiveUpdate;
17 use namespace::autoclean;
21 isa => JSON::MaybeXS::JSON(),
27 # no ->utf8 here because the request params get decoded by Catalyst
28 return JSON::MaybeXS->new;
31 with 'Catalyst::Controller::DBIC::API::StoredResultSource',
32 'Catalyst::Controller::DBIC::API::StaticArguments';
34 with 'Catalyst::Controller::DBIC::API::RequestArguments' => { static => 1 };
36 __PACKAGE__->config();
40 package MyApp::Controller::API::RPC::Artist;
42 BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC' }
45 ( # define parent chain action and PathPart
48 Chained => '/api/rpc/rpc_base',
52 class => 'MyAppDB::Artist',
53 resultset_class => 'MyAppDB::ResultSet::Artist',
54 create_requires => ['name', 'age'],
55 create_allows => ['nickname'],
56 update_allows => ['name', 'age', 'nickname'],
57 update_allows => ['name', 'age', 'nickname'],
58 select => ['name', 'age'],
63 { cds => ['tracks'] },
65 ordered_by => ['age'],
66 search_exposes => ['age', 'nickname', { cds => ['title', 'year'] }],
69 use_json_boolean => 1,
73 # Provides the following functional endpoints:
74 # /api/rpc/artist/create
75 # /api/rpc/artist/list
76 # /api/rpc/artist/id/[id]/delete
77 # /api/rpc/artist/id/[id]/update
84 begin is provided in the base class to setup the Catalyst request object by
85 applying the DBIC::API::Request role.
90 my ( $self, $c ) = @_;
92 Moose::Util::ensure_all_roles( $c->req,
93 'Catalyst::Controller::DBIC::API::Request' );
96 =method_protected setup
98 :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('specify.in.subclass.config')
100 This action is the chain root of the controller. It must either be overridden or
101 configured to provide a base PathPart to the action and also a parent action.
102 For example, for class MyAppDB::Track you might have
104 package MyApp::Controller::API::RPC::Track;
106 BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC'; }
109 ( action => { setup => { PathPart => 'track', Chained => '/api/rpc/rpc_base' } },
115 sub setup :Chained('/api/rpc/rpc_base') :CaptureArgs(0) :PathPart('track') {
118 $self->next::method($c);
121 This action does nothing by default.
125 sub setup : Chained('specify.in.subclass.config') : CaptureArgs(0) :
126 PathPart('specify.in.subclass.config') { }
128 =method_protected deserialize
130 :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('Deserialize')
132 Absorbs the request data and transforms it into useful bits by using
133 CGI::Expand->expand_hash and a smattering of JSON->decode for a handful of
136 Current only the following arguments are capable of being expressed as JSON:
145 It should be noted that arguments can used mixed modes in with some caveats.
146 Each top level arg can be expressed as CGI::Expand with their immediate child
147 keys expressed as JSON when sending the data application/x-www-form-urlencoded.
148 Otherwise, you can send content as raw json and it will be deserialized as is
149 with no CGI::Expand expasion.
153 sub deserialize : Chained('setup') : CaptureArgs(0) : PathPart('') :
154 ActionClass('Deserialize') {
155 my ( $self, $c ) = @_;
158 if ( $c->req->data && scalar( keys %{ $c->req->data } ) ) {
159 $req_params = $c->req->data;
162 $req_params = CGI::Expand->expand_hash( $c->req->params );
165 @{ [ $self->search_arg, $self->count_arg,
166 $self->page_arg, $self->offset_arg,
167 $self->ordered_by_arg, $self->grouped_by_arg,
173 # these params can also be composed of JSON
174 # but skip if the parameter is not provided
175 next if not exists $req_params->{$param};
177 # find out if CGI::Expand was involved
178 if ( ref $req_params->{$param} eq 'HASH' ) {
179 for my $key ( keys %{ $req_params->{$param} } ) {
181 # copy the value because JSON::XS will alter it
182 # even if decoding failed
183 my $value = $req_params->{$param}->{$key};
185 my $deserialized = $self->_json->decode($value);
186 $req_params->{$param}->{$key} = $deserialized;
190 "Param '$param.$key' did not deserialize appropriately: $_"
197 my $value = $req_params->{$param};
198 my $deserialized = $self->_json->decode($value);
199 $req_params->{$param} = $deserialized;
203 "Param '$param' did not deserialize appropriately: $_"
210 $self->inflate_request( $c, $req_params );
213 =method_protected generate_rs
215 generate_rs is used by inflate_request to get a resultset for the current
216 request. It receives $c as its only argument.
217 By default it returns a resultset of the controller's class.
218 Override this method if you need to manipulate the default implementation of
224 my ( $self, $c ) = @_;
226 return $c->model( $c->stash->{class} || $self->class );
229 =method_protected inflate_request
231 inflate_request is called at the end of deserialize to populate key portions of
232 the request with the useful bits.
236 sub inflate_request {
237 my ( $self, $c, $params ) = @_;
240 # set static arguments
241 $c->req->_set_controller($self);
243 # set request arguments
244 $c->req->_set_request_data($params);
246 # set the current resultset
247 $c->req->_set_current_result_set( $self->generate_rs($c) );
252 $self->push_error( $c, { message => $_ } );
257 =method_protected object_with_id
259 :Chained('deserialize') :CaptureArgs(1) :PathPart('')
261 This action is the chain root for all object level actions (such as delete and
262 update) that operate on a single identifer. The provided identifier will be used
263 to find that particular object and add it to the request's store ofobjects.
265 Please see L<Catalyst::Controller::DBIC::API::Request::Context> for more
266 details on the stored objects.
270 sub object_with_id : Chained('deserialize') : CaptureArgs(1) : PathPart('') {
271 my ( $self, $c, $id ) = @_;
273 my $vals = $c->req->request_data->{ $self->data_root };
274 unless ( defined($vals) ) {
276 # no data root, assume the request_data itself is the payload
277 $vals = $c->req->request_data;
281 # there can be only one set of data
282 $c->req->add_object( [ $self->object_lookup( $c, $id ), $vals ] );
286 $self->push_error( $c, { message => $_ } );
291 =method_protected objects_no_id
293 :Chained('deserialize') :CaptureArgs(0) :PathPart('')
295 This action is the chain root for object level actions (such as create, update,
296 or delete) that can involve more than one object. The data stored at the
297 data_root of the request_data will be interpreted as an array of hashes on which
298 to operate. If the hashes are missing an 'id' key, they will be considered a
299 new object to be created. Otherwise, the values in the hash will be used to
300 perform an update. As a special case, a single hash sent will be coerced into
303 Please see L<Catalyst::Controller::DBIC::API::Request::Context> for more
304 details on the stored objects.
308 sub objects_no_id : Chained('deserialize') : CaptureArgs(0) : PathPart('') {
309 my ( $self, $c ) = @_;
311 if ( $c->req->has_request_data ) {
312 my $data = $c->req->request_data;
315 if ( exists( $data->{ $self->data_root } )
316 && defined( $data->{ $self->data_root } ) )
318 my $root = $data->{ $self->data_root };
319 if ( reftype($root) eq 'ARRAY' ) {
322 elsif ( reftype($root) eq 'HASH' ) {
326 $c->log->error('Invalid request data');
327 $self->push_error( $c,
328 { message => 'Invalid request data' } );
333 # no data root, assume the request_data itself is the payload
334 $vals = [ $c->req->request_data ];
337 foreach my $val (@$vals) {
338 unless ( exists( $val->{id} ) ) {
340 [ $c->req->current_result_set->new_result( {} ), $val ] );
346 [ $self->object_lookup( $c, $val->{id} ), $val ] );
350 $self->push_error( $c, { message => $_ } );
357 =method_protected object_lookup
359 This method provides the look up functionality for an object based on 'id'.
360 It is passed the current $c and the id to be used to perform the lookup.
361 Dies if there is no provided id or if no object was found.
366 my ( $self, $c, $id ) = @_;
368 die 'No valid ID provided for look up' unless defined $id and length $id;
369 my $object = $c->req->current_result_set->find($id);
370 die "No object found for id '$id'" unless defined $object;
374 =method_protected list
376 list's steps are broken up into three distinct methods:
380 =item L</list_munge_parameters>
382 =item L</list_perform_search>
384 =item L</list_format_output>.
388 The goal of this method is to call ->search() on the current_result_set,
389 change the resultset class of the result (if needed), and return it in
390 $c->stash->{$self->stash_key}->{$self->data_root}.
392 Please see the individual methods for more details on what actual processing
395 If the L</select> config param is defined then the hashes will contain only
396 those columns, otherwise all columns in the object will be returned.
397 L</select> of course supports the function/procedure calling semantics that
398 L<DBIx::Class::ResultSet/select> supports.
400 In order to have proper column names in the result, provide arguments in L</as>
401 (which also follows L<DBIx::Class::ResultSet/as> semantics.
402 Similarly L</count>, L</page>, L</grouped_by> and L</ordered_by> affect the
403 maximum number of rows returned as well as the ordering and grouping.
405 Note that if select, count, ordered_by or grouped_by request parameters are
406 present, these will override the values set on the class with select becoming
407 bound by the select_exposes attribute.
409 If not all objects in the resultset are required then it's possible to pass
410 conditions to the method as request parameters. You can use a JSON string as
411 the 'search' parameter for maximum flexibility or use L<CGI::Expand> syntax.
412 In the second case the request parameters are expanded into a structure and
413 then used as the search condition.
415 For example, these request parameters:
417 ?search.name=fred&search.cd.artist=luke
419 ?search={"name":"fred","cd": {"artist":"luke"}}
421 Would result in this search (where 'name' is a column of the result class, 'cd'
422 is a relation of the result class and 'artist' is a column of the related class):
424 $rs->search({ name => 'fred', 'cd.artist' => 'luke' }, { join => ['cd'] })
426 It is also possible to use a JSON string for expandeded parameters:
428 ?search.datetime={"-between":["2010-01-06 19:28:00","2010-01-07 19:28:00"]}
430 Note that if pagination is needed, this can be achieved using a combination of
431 the L</count> and L</page> parameters. For example:
435 Would result in this search:
437 $rs->search({}, { page => 2, rows => 20 })
442 my ( $self, $c ) = @_;
444 $self->list_munge_parameters($c);
445 $self->list_perform_search($c);
446 $self->list_format_output($c);
448 # make sure there are no objects lingering
449 $c->req->clear_objects();
452 =method_protected list_munge_parameters
454 list_munge_parameters is a noop by default. All arguments will be passed through
455 without any manipulation. In order to successfully manipulate the parameters
456 before the search is performed, simply access
457 $c->req->search_parameters|search_attributes (ArrayRef and HashRef respectively),
458 which correspond directly to ->search($parameters, $attributes).
459 Parameter keys will be in already-aliased form.
460 To store the munged parameters call $c->req->_set_search_parameters($newparams)
461 and $c->req->_set_search_attributes($newattrs).
465 sub list_munge_parameters { } # noop by default
467 =method_protected list_perform_search
469 list_perform_search executes the actual search. current_result_set is updated to
470 contain the result returned from ->search. If paging was requested,
471 search_total_entries will be set as well.
475 sub list_perform_search {
476 my ( $self, $c ) = @_;
482 $req->current_result_set->search( $req->search_parameters,
483 $req->search_attributes );
485 $req->_set_current_result_set($rs);
487 $req->_set_search_total_entries(
488 $req->current_result_set->pager->total_entries )
489 if $req->has_search_attributes
490 && ( exists( $req->search_attributes->{page} )
491 && defined( $req->search_attributes->{page} )
492 && length( $req->search_attributes->{page} ) );
496 $self->push_error( $c,
497 { message => 'a database error has occured.' } );
502 =method_protected list_format_output
504 list_format_output prepares the response for transmission across the wire.
505 A copy of the current_result_set is taken and its result_class is set to
506 L<DBIx::Class::ResultClass::HashRefInflator>. Each row in the resultset is then
507 iterated and passed to L</row_format_output> with the result of that call added
512 sub list_format_output {
513 my ( $self, $c ) = @_;
515 my $rs = $c->req->current_result_set->search;
516 $rs->result_class( $self->result_class ) if $self->result_class;
522 foreach my $row ( $rs->all ) {
523 push( @$formatted, $self->row_format_output( $c, $row ) );
526 $output->{ $self->data_root } = $formatted;
528 if ( $c->req->has_search_total_entries ) {
529 $output->{ $self->total_entries_arg } =
530 $c->req->search_total_entries + 0;
533 $c->stash->{ $self->stash_key } = $output;
537 $self->push_error( $c,
538 { message => 'a database error has occured.' } );
543 =method_protected row_format_output
545 row_format_output is called each row of the inflated output generated from the
546 search. It receives two arguments, the catalyst context and the hashref that
547 represents the row. By default, this method is merely a passthrough.
551 sub row_format_output {
553 #my ($self, $c, $row) = @_;
554 my ( $self, undef, $row ) = @_;
555 return $row; # passthrough by default
558 =method_protected item
560 item will return a single object called by identifier in the uri. It will be
561 inflated via each_object_inflate.
566 my ( $self, $c ) = @_;
568 if ( $c->req->count_objects != 1 ) {
570 $self->push_error( $c,
571 { message => 'No objects on which to operate' } );
575 $c->stash->{ $self->stash_key }->{ $self->item_root } =
576 $self->each_object_inflate( $c, $c->req->get_object(0)->[0] );
580 =method_protected update_or_create
582 update_or_create is responsible for iterating any stored objects and performing
583 updates or creates. Each object is first validated to ensure it meets the
584 criteria specified in the L</create_requires> and L</create_allows> (or
585 L</update_allows>) parameters of the controller config. The objects are then
586 committed within a transaction via L</transact_objects> using a closure around
591 sub update_or_create {
592 my ( $self, $c ) = @_;
594 if ( $c->req->has_objects ) {
595 $self->validate_objects($c);
596 $self->transact_objects( $c, sub { $self->save_objects( $c, @_ ) } );
600 $self->push_error( $c,
601 { message => 'No objects on which to operate' } );
606 =method_protected transact_objects
608 transact_objects performs the actual commit to the database via $schema->txn_do.
609 This method accepts two arguments, the context and a coderef to be used within
610 the transaction. All of the stored objects are passed as an arrayref for the
611 only argument to the coderef.
615 sub transact_objects {
616 my ( $self, $c, $coderef ) = @_;
619 $self->stored_result_source->schema->txn_do( $coderef,
624 $self->push_error( $c,
625 { message => 'a database error has occured.' } );
630 =method_protected validate_objects
632 This is a shortcut method for performing validation on all of the stored objects
633 in the request. Each object's provided values (for create or update) are updated
634 to the allowed values permitted by the various config parameters.
638 sub validate_objects {
639 my ( $self, $c ) = @_;
642 foreach my $obj ( $c->req->all_objects ) {
643 $obj->[1] = $self->validate_object( $c, $obj );
648 $c->log->error($err);
649 $err =~ s/\s+at\s+.+\n$//g;
650 $self->push_error( $c, { message => $err } );
655 =method_protected validate_object
657 validate_object takes the context and the object as an argument. It then filters
658 the passed values in slot two of the tuple through the create|update_allows
659 configured. It then returns those filtered values. Values that are not allowed
660 are silently ignored. If there are no values for a particular key, no valid
661 values at all, or multiple of the same key, this method will die.
665 sub validate_object {
666 my ( $self, $c, $obj ) = @_;
667 my ( $object, $params ) = @$obj;
670 my %requires_map = map { $_ => 1 } @{
671 ( $object->in_storage )
673 : $c->stash->{create_requires} || $self->create_requires
676 my %allows_map = map { ( ref $_ ) ? %{$_} : ( $_ => 1 ) } (
678 @{ ( $object->in_storage )
679 ? ( $c->stash->{update_allows} || $self->update_allows )
680 : ( $c->stash->{create_allows} || $self->create_allows )
684 foreach my $key ( keys %allows_map ) {
686 # check value defined if key required
687 my $allowed_fields = $allows_map{$key};
689 if ( ref $allowed_fields ) {
690 my $related_source = $object->result_source->related_source($key);
691 my $related_params = $params->{$key};
692 my %allowed_related_map = map { $_ => 1 } @$allowed_fields;
693 my $allowed_related_cols =
694 ( $allowed_related_map{'*'} )
695 ? [ $related_source->columns ]
698 if (ref($related_params) && reftype($related_params) eq 'ARRAY') {
700 for my $related_param (@$related_params) {
702 foreach my $related_col ( @{$allowed_related_cols} ) {
704 my $related_col_value =
705 $related_param->{$related_col}
708 $data{$related_col} = $related_col_value;
711 push @related_data, \%data;
713 $values{$key} = \@related_data;
716 foreach my $related_col ( @{$allowed_related_cols} ) {
718 my $related_col_value =
719 $related_params->{$related_col}
722 $values{$key}{$related_col} = $related_col_value;
728 my $value = $params->{$key};
730 if ( $requires_map{$key} ) {
731 unless ( defined($value) ) {
733 # if not defined look for default
734 $value = $object->result_source->column_info($key)
736 unless ( defined $value ) {
737 die "No value supplied for ${key} and no default";
742 # check for multiple values
743 if ( ref($value) && !( reftype($value) eq reftype(JSON::MaybeXS::true) ) )
745 require Data::Dumper;
747 "Multiple values for '${key}': ${\Data::Dumper::Dumper($value)}";
750 # check exists so we don't just end up with hash of undefs
751 # check defined to account for default values being used
752 $values{$key} = $value
753 if exists $params->{$key} || defined $value;
757 unless ( keys %values || !$object->in_storage ) {
758 die 'No valid keys passed';
764 =method_protected delete
766 delete operates on the stored objects in the request. It first transacts the
767 objects, deleting them in the database using L</transact_objects> and a closure
768 around L</delete_objects>, and then clears the request store of objects.
773 my ( $self, $c ) = @_;
775 if ( $c->req->has_objects ) {
776 $self->transact_objects( $c,
777 sub { $self->delete_objects( $c, @_ ) } );
778 $c->req->clear_objects;
782 $self->push_error( $c,
783 { message => 'No objects on which to operate' } );
788 =method_protected save_objects
790 This method is used by update_or_create to perform the actual database
791 manipulations. It iterates each object calling L</save_object>.
796 my ( $self, $c, $objects ) = @_;
798 foreach my $obj (@$objects) {
799 $self->save_object( $c, $obj );
803 =method_protected save_object
805 save_object first checks to see if the object is already in storage. If so, it
806 calls L</update_object_from_params> otherwise L</insert_object_from_params>.
811 my ( $self, $c, $obj ) = @_;
813 my ( $object, $params ) = @$obj;
815 if ( $object->in_storage ) {
816 $self->update_object_from_params( $c, $object, $params );
819 $self->insert_object_from_params( $c, $object, $params );
824 =method_protected update_object_from_params
826 update_object_from_params iterates through the params to see if any of them are
827 pertinent to relations. If so it calls L</update_object_relation> with the
828 object, and the relation parameters. Then it calls ->update on the object.
832 sub update_object_from_params {
833 my ( $self, $c, $object, $params ) = @_;
835 $params = {%$params, %{$object->ident_condition}};
838 DBIx::Class::ResultSet::RecursiveUpdate::Functions::recursive_update(
839 resultset => $c->req->current_result_set,
840 # unknown_params_ok => 1,
844 # replace request object with updated one for response
845 my $vals = $c->req->get_object(0)->[1];
846 $c->req->clear_objects;
847 $c->req->add_object( [ $updated_object, $vals ] );
850 =method_protected insert_object_from_params
852 Sets the columns of the object, then calls ->insert.
856 sub insert_object_from_params {
858 #my ($self, $c, $object, $params) = @_;
859 my ( $self, undef, $object, $params ) = @_;
862 while ( my ( $key, $value ) = each %{$params} ) {
863 if ( ref($value) && !( reftype($value) eq reftype(JSON::MaybeXS::true) ) ) {
864 $rels{$key} = $value;
868 elsif ( $object->can($key) ) {
869 $object->$key($value);
872 # accessor != colname
875 $object->result_source->column_info($key)->{accessor};
876 $object->$accessor($value);
882 while ( my ( $k, $v ) = each %rels ) {
883 if (reftype($v) eq 'ARRAY') {
884 foreach my $next_v ( @$v ) {
885 $object->create_related($k, $next_v);
889 $object->create_related($k => $v);
894 =method_protected delete_objects
896 Iterates through each object calling L</delete_object>.
901 my ( $self, $c, $objects ) = @_;
903 map { $self->delete_object( $c, $_->[0] ) } @$objects;
906 =method_protected delete_object
908 Performs the actual ->delete on the object.
914 #my ($self, $c, $object) = @_;
915 my ( $self, undef, $object ) = @_;
920 =method_protected end
922 end performs the final manipulation of the response before it is serialized.
923 This includes setting the success of the request both at the HTTP layer and
924 JSON layer. If configured with return_object true, and there are stored objects
925 as the result of create or update, those will be inflated according to the
926 schema and get_inflated_columns
931 my ( $self, $c ) = @_;
933 # don't change the http status code if already set elsewhere
934 unless ( $c->res->status && $c->res->status != 200 ) {
935 if ( $self->has_errors($c) ) {
936 $c->res->status(400);
939 $c->res->status(200);
943 if ( $c->res->status == 200 ) {
944 $c->stash->{ $self->stash_key }->{success} =
945 $self->use_json_boolean ? JSON::MaybeXS::true : 'true';
946 if ( $self->return_object
947 && $c->req->has_objects
948 && ! exists $c->stash->{ $self->stash_key }->{ $self->data_root } ) {
949 my $returned_objects = [];
950 push( @$returned_objects, $self->each_object_inflate( $c, $_ ) )
951 for map { $_->[0] } $c->req->all_objects;
952 $c->stash->{ $self->stash_key }->{ $self->data_root } =
953 scalar(@$returned_objects) > 1
955 : $returned_objects->[0];
959 $c->stash->{ $self->stash_key }->{success} =
960 $self->use_json_boolean ? JSON::MaybeXS::false : 'false';
961 $c->stash->{ $self->stash_key }->{messages} = $self->get_errors($c)
962 if $self->has_errors($c);
964 # don't return data for error responses
965 delete $c->stash->{ $self->stash_key }->{ $self->data_root };
968 $c->forward('serialize');
971 =method_protected each_object_inflate
973 each_object_inflate executes during L</end> and allows hooking into the process
974 of inflating the objects to return in the response. Receives, the context, and
975 the object as arguments.
977 This only executes if L</return_object> if set and if there are any objects to
982 sub each_object_inflate {
984 #my ($self, $c, $object) = @_;
985 my ( $self, undef, $object ) = @_;
987 return { $object->get_columns };
990 =method_protected serialize
992 multiple actions forward to serialize which uses Catalyst::Action::Serialize.
996 # from Catalyst::Action::Serialize
997 sub serialize : ActionClass('Serialize') { }
999 =method_protected push_error
1001 Stores an error message into the stash to be later retrieved by L</end>.
1002 Accepts a Dict[message => Str] parameter that defines the error message.
1007 my ( $self, $c, $params ) = @_;
1008 die 'Catalyst app object missing'
1010 my $error = 'unknown error';
1011 if ( exists $params->{message} ) {
1012 $error = $params->{message};
1014 # remove newline from die "error message\n" which is required to not
1015 # have the filename and line number in the error text
1018 push( @{ $c->stash->{_dbic_crud_errors} }, $error );
1021 =method_protected get_errors
1023 Returns all of the errors stored in the stash.
1028 my ( $self, $c ) = @_;
1029 die 'Catalyst app object missing'
1031 return $c->stash->{_dbic_crud_errors};
1034 =method_protected has_errors
1036 Returns true if errors are stored in the stash.
1041 my ( $self, $c ) = @_;
1042 die 'Catalyst app object missing'
1044 return exists $c->stash->{_dbic_crud_errors};
1049 Easily provide common API endpoints based on your L<DBIx::Class> schema classes.
1050 Module provides both RPC and REST interfaces to base functionality.
1051 Uses L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize> to
1052 serialize response and/or deserialise request.
1056 This document describes base functionlity such as list, create, delete, update
1057 and the setting of config attributes. L<Catalyst::Controller::DBIC::API::RPC>
1058 and L<Catalyst::Controller::DBIC::API::REST> describe details of provided
1059 endpoints to those base methods.
1061 You will need to create a controller for each schema class you require API
1062 endpoints for. For example if your schema has Artist and Track, and you want to
1063 provide a RESTful interface to these, you should create
1064 MyApp::Controller::API::REST::Artist and MyApp::Controller::API::REST::Track
1065 which both subclass L<Catalyst::Controller::DBIC::API::REST>.
1066 Similarly if you wanted to provide an RPC style interface then subclass
1067 L<Catalyst::Controller::DBIC::API::RPC>. You then configure these individually
1068 as specified in L</CONFIGURATION>.
1070 Also note that the test suite of this module has an example application used to
1071 run tests against. It maybe helpful to look at that until a better tutorial is
1074 =head2 CONFIGURATION
1076 Each of your controller classes needs to be configured to point at the relevant
1077 schema class, specify what can be updated and so on, as shown in the L</SYNOPSIS>.
1079 The class, create_requires, create_allows and update_requires parameters can
1080 also be set in the stash like so:
1082 sub setup :Chained('/api/rpc/rpc_base') :CaptureArgs(1) :PathPart('any') {
1083 my ($self, $c, $object_type) = @_;
1085 if ($object_type eq 'artist') {
1086 $c->stash->{class} = 'MyAppDB::Artist';
1087 $c->stash->{create_requires} = [qw/name/];
1088 $c->stash->{update_allows} = [qw/name/];
1090 $self->push_error($c, { message => "invalid object_type" });
1094 $self->next::method($c);
1097 Generally it's better to have one controller for each DBIC source with the
1098 config hardcoded, but in some cases this isn't possible.
1100 Note that the Chained, CaptureArgs and PathPart are just standard Catalyst
1101 configuration parameters and that then endpoint specified in Chained - in this
1102 case '/api/rpc/rpc_base' - must actually exist elsewhere in your application.
1103 See L<Catalyst::DispatchType::Chained> for more details.
1105 Below are explanations for various configuration parameters. Please see
1106 L<Catalyst::Controller::DBIC::API::StaticArguments> for more details.
1110 Whatever you would pass to $c->model to get a resultset for this class.
1111 MyAppDB::Track for example.
1113 =head3 resultset_class
1115 Desired resultset class after accessing your model. MyAppDB::ResultSet::Track
1116 for example. By default, it's DBIx::Class::ResultClass::HashRefInflator.
1117 Set to empty string to leave resultset class without change.
1121 Controls where in stash request_data should be stored, and defaults to 'response'.
1125 By default, the response data of multiple item actions is serialized into
1126 $c->stash->{$self->stash_key}->{$self->data_root} and data_root defaults to
1127 'list' to preserve backwards compatibility. This is now configuable to meet
1128 the needs of the consuming client.
1132 By default, the response data of single item actions is serialized into
1133 $c->stash->{$self->stash_key}->{$self->item_root} and item_root default to
1136 =head3 use_json_boolean
1138 By default, the response success status is set to a string value of "true" or
1139 "false". If this attribute is true, JSON::MaybeXS's true() and false() will be
1140 used instead. Note, this does not effect other internal processing of boolean
1143 =head3 count_arg, page_arg, select_arg, search_arg, grouped_by_arg, ordered_by_arg, prefetch_arg, as_arg, total_entries_arg
1145 These attributes allow customization of the component to understand requests
1146 made by clients where these argument names are not flexible and cannot conform
1147 to this components defaults.
1149 =head3 create_requires
1151 Arrayref listing columns required to be passed to create in order for the
1152 request to be valid.
1154 =head3 create_allows
1156 Arrayref listing columns additional to those specified in create_requires that
1157 are not required to create but which create does allow. Columns passed to create
1158 that are not listed in create_allows or create_requires will be ignored.
1160 =head3 update_allows
1162 Arrayref listing columns that update will allow. Columns passed to update that
1163 are not listed here will be ignored.
1167 Arguments to pass to L<DBIx::Class::ResultSet/select> when performing search for
1172 Complements arguments passed to L<DBIx::Class::ResultSet/select> when performing
1173 a search. This allows you to specify column names in the result for RDBMS
1176 =head3 select_exposes
1178 Columns and related columns that are okay to return in the resultset since
1179 clients can request more or less information specified than the above select
1184 Arguments to pass to L<DBIx::Class::ResultSet/prefetch> when performing search
1187 =head3 prefetch_allows
1189 Arrayref listing relationships that are allowed to be prefetched.
1190 This is necessary to avoid denial of service attacks in form of
1191 queries which would return a large number of data
1192 and unwanted disclosure of data.
1196 Arguments to pass to L<DBIx::Class::ResultSet/group_by> when performing search
1201 Arguments to pass to L<DBIx::Class::ResultSet/order_by> when performing search
1204 =head3 search_exposes
1206 Columns and related columns that are okay to search on. For example if only the
1207 position column and all cd columns were to be allowed
1209 search_exposes => [qw/position/, { cd => ['*'] }]
1211 You can also use this to allow custom columns should you wish to allow them
1212 through in order to be caught by a custom resultset. For example:
1214 package RestTest::Controller::API::RPC::TrackExposed;
1220 search_exposes => [qw/position title custom_column/],
1223 and then in your custom resultset:
1225 package RestTest::Schema::ResultSet::Track;
1227 use base 'RestTest::Schema::ResultSet';
1231 my ($clause, $params) = @_;
1234 if (my $pretend = delete $clause->{custom_column}) {
1235 $clause->{'cd.year'} = $pretend;
1237 my $rs = $self->SUPER::search(@_);
1242 Arguments to pass to L<DBIx::Class::ResultSet/rows> when performing search for
1247 Arguments to pass to L<DBIx::Class::ResultSet/page> when performing search for
1252 By default the create, delete and update actions will not return anything apart
1253 from the success parameter set in L</end>, often this is not ideal but the
1254 required behaviour varies from application to application. So normally it's
1255 sensible to write an intermediate class which your main controller classes
1258 For example if you wanted create to return the JSON for the newly created
1259 object you might have something like:
1261 package MyApp::ControllerBase::DBIC::API::RPC;
1264 BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC' };
1266 sub create :Chained('setup') :Args(0) :PathPart('create') {
1267 my ($self, $c) = @_;
1269 # $c->req->all_objects will contain all of the created
1270 $self->next::method($c);
1272 if ($c->req->has_objects) {
1273 # $c->stash->{$self->stash_key} will be serialized in the end action
1274 $c->stash->{$self->stash_key}->{$self->data_root} = [ map { { $_->get_inflated_columns } } ($c->req->all_objects) ] ;
1278 package MyApp::Controller::API::RPC::Track;
1281 BEGIN { extends 'MyApp::ControllerBase::DBIC::API::RPC' };
1284 It should be noted that the return_object attribute will produce the above
1285 result for you, free of charge.
1287 Similarly you might want create, update and delete to all forward to the list
1288 action once they are done so you can refresh your view. This should also be
1291 If more extensive customization is required, it is recommened to peer into the
1292 roles that comprise the system and make use
1296 It should be noted that version 1.004 and above makes a rapid depature from the
1297 status quo. The internals were revamped to use more modern tools such as Moose
1298 and its role system to refactor functionality out into self-contained roles.
1300 To this end, internally, this module now understands JSON boolean values (as
1301 represented by the JSON::MaybeXS module) and will Do The Right Thing in
1302 handling those values. This means you can have ColumnInflators installed that
1303 can covert between JSON booleans and whatever your database wants for boolean
1306 Validation for various *_allows or *_exposes is now accomplished via
1307 Data::DPath::Validator with a lightly simplified, via a subclass of
1308 Data::DPath::Validator::Visitor.
1310 The rough jist of the process goes as follows: Arguments provided to those
1311 attributes are fed into the Validator and Data::DPaths are generated.
1312 Then incoming requests are validated against these paths generated.
1313 The validator is set in "loose" mode meaning only one path is required to match.
1314 For more information, please see L<Data::DPath::Validator> and more specifically
1315 L<Catalyst::Controller::DBIC::API::Validator>.
1318 Transactions are used. The stash is put aside in favor of roles applied to the
1319 request object with additional accessors.
1320 Error handling is now much more consistent with most errors immediately detaching.
1321 The internals are much easier to read and understand with lots more documentation.
1324 The SQL::Abstract -and, -not and -or operators are supported.