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