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