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