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