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