fix links to ::Request::Context
[catagits/Catalyst-Controller-DBIC-API.git] / lib / Catalyst / Controller / DBIC / API.pm
CommitLineData
d2739840 1package Catalyst::Controller::DBIC::API;
2
3#ABSTRACT: Provides a DBIx::Class web service automagically
4use Moose;
26e9dcd6 5BEGIN { extends 'Catalyst::Controller'; }
d2739840 6
7use CGI::Expand ();
8use DBIx::Class::ResultClass::HashRefInflator;
53429220 9use JSON ();
d2739840 10use Test::Deep::NoTest('eq_deeply');
11use MooseX::Types::Moose(':all');
12use Moose::Util;
8ea592cb 13use Scalar::Util( 'blessed', 'reftype' );
d2739840 14use Try::Tiny;
15use Catalyst::Controller::DBIC::API::Request;
16use namespace::autoclean;
17
0b0bf911 18has '_json' => (
8ea592cb 19 is => 'ro',
20 isa => 'JSON',
0b0bf911 21 lazy_build => 1,
22);
23
24sub _build__json {
8ea592cb 25
0b0bf911 26 # no ->utf8 here because the request params get decoded by Catalyst
27 return JSON->new;
28}
29
d6993542 30with 'Catalyst::Controller::DBIC::API::StoredResultSource',
8ea592cb 31 'Catalyst::Controller::DBIC::API::StaticArguments';
4e5983f2 32
33with 'Catalyst::Controller::DBIC::API::RequestArguments' => { static => 1 };
d2739840 34
35__PACKAGE__->config();
36
37=head1 SYNOPSIS
38
39 package MyApp::Controller::API::RPC::Artist;
40 use Moose;
41 BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC' }
42
43 __PACKAGE__->config
8ea592cb 44 ( # define parent chain action and PathPart
45 action => {
46 setup => {
47 Chained => '/api/rpc/rpc_base',
48 PathPart => 'artist',
49 }
50 },
d6993542 51 class => 'MyAppDB::Artist',
67431358 52 resultset_class => 'MyAppDB::ResultSet::Artist',
d6993542 53 create_requires => ['name', 'age'],
54 create_allows => ['nickname'],
55 update_allows => ['name', 'age', 'nickname'],
56 update_allows => ['name', 'age', 'nickname'],
8ea592cb 57 select => ['name', 'age'],
d6993542 58 prefetch => ['cds'],
59 prefetch_allows => [
d2739840 60 'cds',
d2739840 61 { cds => 'tracks' },
8ea592cb 62 { cds => ['tracks'] },
d2739840 63 ],
8ea592cb 64 ordered_by => ['age'],
65 search_exposes => ['age', 'nickname', { cds => ['title', 'year'] }],
d6993542 66 data_root => 'data',
67 use_json_boolean => 1,
68 return_object => 1,
d2739840 69 );
70
71 # Provides the following functional endpoints:
72 # /api/rpc/artist/create
73 # /api/rpc/artist/list
74 # /api/rpc/artist/id/[id]/delete
75 # /api/rpc/artist/id/[id]/update
76=cut
77
533075c7 78=method_private begin
79
80 :Private
81
c0c8e1c6 82begin is provided in the base class to setup the Catalyst request object by
83applying the DBIC::API::Request role.
533075c7 84
85=cut
86
8ea592cb 87sub begin : Private {
88 my ( $self, $c ) = @_;
533075c7 89
8ea592cb 90 Moose::Util::ensure_all_roles( $c->req,
91 'Catalyst::Controller::DBIC::API::Request' );
533075c7 92}
93
d2739840 94=method_protected setup
95
96 :Chained('specify.in.subclass.config') :CaptureArgs(0) :PathPart('specify.in.subclass.config')
97
8ea592cb 98This action is the chain root of the controller. It must either be overridden or
99configured to provide a base PathPart to the action and also a parent action.
100For example, for class MyAppDB::Track you might have
d2739840 101
102 package MyApp::Controller::API::RPC::Track;
406086f3 103 use Moose;
533075c7 104 BEGIN { extends 'Catalyst::Controller::DBIC::API::RPC'; }
d2739840 105
106 __PACKAGE__->config
406086f3 107 ( action => { setup => { PathPart => 'track', Chained => '/api/rpc/rpc_base' } },
d2739840 108 ...
109 );
110
111 # or
112
5fca1d92 113 sub setup :Chained('/api/rpc/rpc_base') :CaptureArgs(0) :PathPart('track') {
d2739840 114 my ($self, $c) = @_;
115
116 $self->next::method($c);
117 }
118
533075c7 119This action does nothing by default.
d2739840 120
121=cut
122
8ea592cb 123sub setup : Chained('specify.in.subclass.config') : CaptureArgs(0) :
124 PathPart('specify.in.subclass.config') { }
d2739840 125
126=method_protected deserialize
127
533075c7 128 :Chained('setup') :CaptureArgs(0) :PathPart('') :ActionClass('Deserialize')
129
c0c8e1c6 130Absorbs the request data and transforms it into useful bits by using
131CGI::Expand->expand_hash and a smattering of JSON->decode for a handful of
132arguments.
133
134Current only the following arguments are capable of being expressed as JSON:
d2739840 135
136 search_arg
137 count_arg
138 page_arg
139 ordered_by_arg
140 grouped_by_arg
141 prefetch_arg
142
c0c8e1c6 143It should be noted that arguments can used mixed modes in with some caveats.
144Each top level arg can be expressed as CGI::Expand with their immediate child
145keys expressed as JSON when sending the data application/x-www-form-urlencoded.
146Otherwise, you can send content as raw json and it will be deserialized as is
147with no CGI::Expand expasion.
d2739840 148
149=cut
150
8ea592cb 151sub deserialize : Chained('setup') : CaptureArgs(0) : PathPart('') :
152 ActionClass('Deserialize') {
153 my ( $self, $c ) = @_;
d2739840 154 my $req_params;
155
8ea592cb 156 if ( $c->req->data && scalar( keys %{ $c->req->data } ) ) {
d2739840 157 $req_params = $c->req->data;
158 }
8ea592cb 159 else {
160 $req_params = CGI::Expand->expand_hash( $c->req->params );
161
162 foreach my $param (
163 @{ [ $self->search_arg, $self->count_arg,
164 $self->page_arg, $self->offset_arg,
165 $self->ordered_by_arg, $self->grouped_by_arg,
166 $self->prefetch_arg
167 ]
168 }
169 )
d2739840 170 {
171 # these params can also be composed of JSON
172 # but skip if the parameter is not provided
173 next if not exists $req_params->{$param};
8ea592cb 174
d2739840 175 # find out if CGI::Expand was involved
8ea592cb 176 if ( ref $req_params->{$param} eq 'HASH' ) {
177 for my $key ( keys %{ $req_params->{$param} } ) {
178
0b0bf911 179 # copy the value because JSON::XS will alter it
180 # even if decoding failed
181 my $value = $req_params->{$param}->{$key};
8ea592cb 182 try {
0b0bf911 183 my $deserialized = $self->_json->decode($value);
d2739840 184 $req_params->{$param}->{$key} = $deserialized;
185 }
8ea592cb 186 catch {
187 $c->log->debug(
188 "Param '$param.$key' did not deserialize appropriately: $_"
189 ) if $c->debug;
d2739840 190 }
191 }
192 }
8ea592cb 193 else {
194 try {
195 my $value = $req_params->{$param};
0b0bf911 196 my $deserialized = $self->_json->decode($value);
d2739840 197 $req_params->{$param} = $deserialized;
198 }
8ea592cb 199 catch {
200 $c->log->debug(
201 "Param '$param' did not deserialize appropriately: $_"
202 ) if $c->debug;
d2739840 203 }
204 }
205 }
206 }
406086f3 207
8ea592cb 208 $self->inflate_request( $c, $req_params );
d2739840 209}
210
9a29ee35 211=method_protected generate_rs
212
5a3fd922 213generate_rs is used by inflate_request to get a resultset for the current
214request. It receives $c as its only argument.
215By default it returns a resultset of the controller's class.
216Override this method if you need to manipulate the default implementation of
217getting a resultset.
9a29ee35 218
219=cut
220
8ea592cb 221sub generate_rs {
222 my ( $self, $c ) = @_;
def4bb3d 223
8ea592cb 224 return $c->model( $c->stash->{class} || $self->class );
9a29ee35 225}
226
d2739840 227=method_protected inflate_request
406086f3 228
c0c8e1c6 229inflate_request is called at the end of deserialize to populate key portions of
230the request with the useful bits.
d2739840 231
232=cut
233
8ea592cb 234sub inflate_request {
235 my ( $self, $c, $params ) = @_;
d2739840 236
8ea592cb 237 try {
d2739840 238 # set static arguments
406086f3 239 $c->req->_set_controller($self);
d2739840 240
241 # set request arguments
242 $c->req->_set_request_data($params);
8cf0b66a 243
244 # set the current resultset
8ea592cb 245 $c->req->_set_current_result_set( $self->generate_rs($c) );
406086f3 246
d2739840 247 }
8ea592cb 248 catch {
d2739840 249 $c->log->error($_);
8ea592cb 250 $self->push_error( $c, { message => $_ } );
d2739840 251 $c->detach();
252 }
8cf0b66a 253}
254
255=method_protected object_with_id
256
257 :Chained('deserialize') :CaptureArgs(1) :PathPart('')
258
c0c8e1c6 259This action is the chain root for all object level actions (such as delete and
260update) that operate on a single identifer. The provided identifier will be used
261to find that particular object and add it to the request's store ofobjects.
262
08e89b6d 263Please see L<Catalyst::Controller::DBIC::API::Request::Context> for more
264details on the stored objects.
8cf0b66a 265
266=cut
267
8ea592cb 268sub object_with_id : Chained('deserialize') : CaptureArgs(1) : PathPart('') {
269 my ( $self, $c, $id ) = @_;
270
271 my $vals = $c->req->request_data->{ $self->data_root };
272 unless ( defined($vals) ) {
8cf0b66a 273
8cf0b66a 274 # no data root, assume the request_data itself is the payload
275 $vals = $c->req->request_data;
276 }
277
8ea592cb 278 try {
8cf0b66a 279 # there can be only one set of data
8ea592cb 280 $c->req->add_object( [ $self->object_lookup( $c, $id ), $vals ] );
8cf0b66a 281 }
8ea592cb 282 catch {
8cf0b66a 283 $c->log->error($_);
8ea592cb 284 $self->push_error( $c, { message => $_ } );
8cf0b66a 285 $c->detach();
286 }
287}
288
289=method_protected objects_no_id
290
291 :Chained('deserialize') :CaptureArgs(0) :PathPart('')
292
c0c8e1c6 293This action is the chain root for object level actions (such as create, update,
294or delete) that can involve more than one object. The data stored at the
295data_root of the request_data will be interpreted as an array of hashes on which
296to operate. If the hashes are missing an 'id' key, they will be considered a
297new object to be created. Otherwise, the values in the hash will be used to
298perform an update. As a special case, a single hash sent will be coerced into
299an array.
300
08e89b6d 301Please see L<Catalyst::Controller::DBIC::API::Request::Context> for more
302details on the stored objects.
8cf0b66a 303
304=cut
305
8ea592cb 306sub objects_no_id : Chained('deserialize') : CaptureArgs(0) : PathPart('') {
307 my ( $self, $c ) = @_;
406086f3 308
8ea592cb 309 if ( $c->req->has_request_data ) {
533075c7 310 my $data = $c->req->request_data;
311 my $vals;
406086f3 312
8ea592cb 313 if ( exists( $data->{ $self->data_root } )
314 && defined( $data->{ $self->data_root } ) )
8cf0b66a 315 {
8ea592cb 316 my $root = $data->{ $self->data_root };
317 if ( reftype($root) eq 'ARRAY' ) {
533075c7 318 $vals = $root;
319 }
8ea592cb 320 elsif ( reftype($root) eq 'HASH' ) {
533075c7 321 $vals = [$root];
322 }
8ea592cb 323 else {
533075c7 324 $c->log->error('Invalid request data');
8ea592cb 325 $self->push_error( $c,
326 { message => 'Invalid request data' } );
533075c7 327 $c->detach();
328 }
8cf0b66a 329 }
8ea592cb 330 else {
533075c7 331 # no data root, assume the request_data itself is the payload
8ea592cb 332 $vals = [ $c->req->request_data ];
8cf0b66a 333 }
533075c7 334
8ea592cb 335 foreach my $val (@$vals) {
336 unless ( exists( $val->{id} ) ) {
337 $c->req->add_object(
338 [ $c->req->current_result_set->new_result( {} ), $val ] );
533075c7 339 next;
340 }
341
8ea592cb 342 try {
343 $c->req->add_object(
344 [ $self->object_lookup( $c, $val->{id} ), $val ] );
533075c7 345 }
8ea592cb 346 catch {
533075c7 347 $c->log->error($_);
8ea592cb 348 $self->push_error( $c, { message => $_ } );
533075c7 349 $c->detach();
350 }
8cf0b66a 351 }
352 }
353}
354
355=method_protected object_lookup
356
c0c8e1c6 357This method provides the look up functionality for an object based on 'id'.
358It is passed the current $c and the id to be used to perform the lookup.
359Dies if there is no provided id or if no object was found.
8cf0b66a 360
361=cut
362
8ea592cb 363sub object_lookup {
364 my ( $self, $c, $id ) = @_;
8cf0b66a 365
366 die 'No valid ID provided for look up' unless defined $id and length $id;
367 my $object = $c->req->current_result_set->find($id);
368 die "No object found for id '$id'" unless defined $object;
369 return $object;
d2739840 370}
371
372=method_protected list
373
c0c8e1c6 374list's steps are broken up into three distinct methods:
375
376=over
377
378=item L</list_munge_parameters>
379
380=item L</list_perform_search>
d2739840 381
c0c8e1c6 382=item L</list_format_output>.
d2739840 383
c0c8e1c6 384=back
d2739840 385
c0c8e1c6 386The goal of this method is to call ->search() on the current_result_set,
387change the resultset class of the result (if needed), and return it in
388$c->stash->{$self->stash_key}->{$self->data_root}.
389
390Please see the individual methods for more details on what actual processing
391takes place.
392
393If the L</select> config param is defined then the hashes will contain only
394those columns, otherwise all columns in the object will be returned.
395L</select> of course supports the function/procedure calling semantics that
396L<DBIx::Class::ResultSet/select> supports.
397
398In order to have proper column names in the result, provide arguments in L</as>
399(which also follows L<DBIx::Class::ResultSet/as> semantics.
400Similarly L</count>, L</page>, L</grouped_by> and L</ordered_by> affect the
401maximum number of rows returned as well as the ordering and grouping.
402
403Note that if select, count, ordered_by or grouped_by request parameters are
404present, these will override the values set on the class with select becoming
405bound by the select_exposes attribute.
406
407If not all objects in the resultset are required then it's possible to pass
408conditions to the method as request parameters. You can use a JSON string as
409the 'search' parameter for maximum flexibility or use L<CGI::Expand> syntax.
410In the second case the request parameters are expanded into a structure and
411then used as the search condition.
d2739840 412
413For example, these request parameters:
414
415 ?search.name=fred&search.cd.artist=luke
416 OR
417 ?search={"name":"fred","cd": {"artist":"luke"}}
418
c0c8e1c6 419Would result in this search (where 'name' is a column of the result class, 'cd'
420is a relation of the result class and 'artist' is a column of the related class):
d2739840 421
422 $rs->search({ name => 'fred', 'cd.artist' => 'luke' }, { join => ['cd'] })
423
424It is also possible to use a JSON string for expandeded parameters:
425
426 ?search.datetime={"-between":["2010-01-06 19:28:00","2010-01-07 19:28:00"]}
427
c0c8e1c6 428Note that if pagination is needed, this can be achieved using a combination of
429the L</count> and L</page> parameters. For example:
d2739840 430
431 ?page=2&count=20
432
433Would result in this search:
406086f3 434
d2739840 435 $rs->search({}, { page => 2, rows => 20 })
436
437=cut
438
8ea592cb 439sub list {
440 my ( $self, $c ) = @_;
d2739840 441
442 $self->list_munge_parameters($c);
443 $self->list_perform_search($c);
444 $self->list_format_output($c);
533075c7 445
446 # make sure there are no objects lingering
406086f3 447 $c->req->clear_objects();
d2739840 448}
449
450=method_protected list_munge_parameters
451
c0c8e1c6 452list_munge_parameters is a noop by default. All arguments will be passed through
453without any manipulation. In order to successfully manipulate the parameters
454before the search is performed, simply access
455$c->req->search_parameters|search_attributes (ArrayRef and HashRef respectively),
456which correspond directly to ->search($parameters, $attributes).
457Parameter keys will be in already-aliased form.
458To store the munged parameters call $c->req->_set_search_parameters($newparams)
459and $c->req->_set_search_attributes($newattrs).
d2739840 460
461=cut
462
8ea592cb 463sub list_munge_parameters { } # noop by default
d2739840 464
465=method_protected list_perform_search
466
c0c8e1c6 467list_perform_search executes the actual search. current_result_set is updated to
468contain the result returned from ->search. If paging was requested,
469search_total_entries will be set as well.
d2739840 470
471=cut
472
8ea592cb 473sub list_perform_search {
474 my ( $self, $c ) = @_;
406086f3 475
8ea592cb 476 try {
d2739840 477 my $req = $c->req;
406086f3 478
8ea592cb 479 my $rs =
480 $req->current_result_set->search( $req->search_parameters,
481 $req->search_attributes );
d2739840 482
483 $req->_set_current_result_set($rs);
484
8ea592cb 485 $req->_set_search_total_entries(
486 $req->current_result_set->pager->total_entries )
487 if $req->has_search_attributes
488 && ( exists( $req->search_attributes->{page} )
489 && defined( $req->search_attributes->{page} )
490 && length( $req->search_attributes->{page} ) );
d2739840 491 }
8ea592cb 492 catch {
d2739840 493 $c->log->error($_);
8ea592cb 494 $self->push_error( $c,
495 { message => 'a database error has occured.' } );
d2739840 496 $c->detach();
497 }
498}
499
500=method_protected list_format_output
501
c0c8e1c6 502list_format_output prepares the response for transmission across the wire.
503A copy of the current_result_set is taken and its result_class is set to
504L<DBIx::Class::ResultClass::HashRefInflator>. Each row in the resultset is then
505iterated and passed to L</row_format_output> with the result of that call added
506to the output.
d2739840 507
508=cut
509
8ea592cb 510sub list_format_output {
511 my ( $self, $c ) = @_;
d2739840 512
513 my $rs = $c->req->current_result_set->search;
8ea592cb 514 $rs->result_class( $self->result_class ) if $self->result_class;
406086f3 515
8ea592cb 516 try {
517 my $output = {};
d2739840 518 my $formatted = [];
406086f3 519
8ea592cb 520 foreach my $row ( $rs->all ) {
521 push( @$formatted, $self->row_format_output( $c, $row ) );
d2739840 522 }
406086f3 523
8ea592cb 524 $output->{ $self->data_root } = $formatted;
d2739840 525
8ea592cb 526 if ( $c->req->has_search_total_entries ) {
527 $output->{ $self->total_entries_arg } =
528 $c->req->search_total_entries + 0;
d2739840 529 }
530
8ea592cb 531 $c->stash->{ $self->stash_key } = $output;
d2739840 532 }
8ea592cb 533 catch {
d2739840 534 $c->log->error($_);
8ea592cb 535 $self->push_error( $c,
536 { message => 'a database error has occured.' } );
d2739840 537 $c->detach();
538 }
539}
540
541=method_protected row_format_output
542
c0c8e1c6 543row_format_output is called each row of the inflated output generated from the
544search. It receives two arguments, the catalyst context and the hashref that
545represents the row. By default, this method is merely a passthrough.
d2739840 546
547=cut
548
8ea592cb 549sub row_format_output {
550
def4bb3d 551 #my ($self, $c, $row) = @_;
8ea592cb 552 my ( $self, undef, $row ) = @_;
553 return $row; # passthrough by default
4cb15235 554}
d2739840 555
609916e5 556=method_protected item
609916e5 557
c0c8e1c6 558item will return a single object called by identifier in the uri. It will be
559inflated via each_object_inflate.
609916e5 560
561=cut
562
8ea592cb 563sub item {
564 my ( $self, $c ) = @_;
609916e5 565
8ea592cb 566 if ( $c->req->count_objects != 1 ) {
609916e5 567 $c->log->error($_);
8ea592cb 568 $self->push_error( $c,
569 { message => 'No objects on which to operate' } );
609916e5 570 $c->detach();
571 }
8ea592cb 572 else {
573 $c->stash->{ $self->stash_key }->{ $self->item_root } =
574 $self->each_object_inflate( $c, $c->req->get_object(0)->[0] );
609916e5 575 }
576}
577
d2739840 578=method_protected update_or_create
579
c0c8e1c6 580update_or_create is responsible for iterating any stored objects and performing
581updates or creates. Each object is first validated to ensure it meets the
582criteria specified in the L</create_requires> and L</create_allows> (or
583L</update_allows>) parameters of the controller config. The objects are then
584committed within a transaction via L</transact_objects> using a closure around
585L</save_objects>.
d2739840 586
587=cut
588
8ea592cb 589sub update_or_create {
590 my ( $self, $c ) = @_;
406086f3 591
8ea592cb 592 if ( $c->req->has_objects ) {
d2739840 593 $self->validate_objects($c);
8ea592cb 594 $self->transact_objects( $c, sub { $self->save_objects( $c, @_ ) } );
d2739840 595 }
8ea592cb 596 else {
d2739840 597 $c->log->error($_);
8ea592cb 598 $self->push_error( $c,
599 { message => 'No objects on which to operate' } );
d2739840 600 $c->detach();
601 }
602}
603
604=method_protected transact_objects
605
c0c8e1c6 606transact_objects performs the actual commit to the database via $schema->txn_do.
607This method accepts two arguments, the context and a coderef to be used within
608the transaction. All of the stored objects are passed as an arrayref for the
609only argument to the coderef.
d2739840 610
611=cut
612
8ea592cb 613sub transact_objects {
614 my ( $self, $c, $coderef ) = @_;
406086f3 615
8ea592cb 616 try {
617 $self->stored_result_source->schema->txn_do( $coderef,
618 $c->req->objects );
d2739840 619 }
8ea592cb 620 catch {
d2739840 621 $c->log->error($_);
8ea592cb 622 $self->push_error( $c,
623 { message => 'a database error has occured.' } );
d2739840 624 $c->detach();
625 }
626}
627
628=method_protected validate_objects
629
c0c8e1c6 630This is a shortcut method for performing validation on all of the stored objects
631in the request. Each object's provided values (for create or update) are updated
632to the allowed values permitted by the various config parameters.
d2739840 633
634=cut
635
8ea592cb 636sub validate_objects {
637 my ( $self, $c ) = @_;
d2739840 638
8ea592cb 639 try {
640 foreach my $obj ( $c->req->all_objects ) {
641 $obj->[1] = $self->validate_object( $c, $obj );
d2739840 642 }
643 }
8ea592cb 644 catch {
d2739840 645 my $err = $_;
646 $c->log->error($err);
bec622aa 647 $err =~ s/\s+at\s+.+\n$//g;
8ea592cb 648 $self->push_error( $c, { message => $err } );
d2739840 649 $c->detach();
650 }
651}
652
653=method_protected validate_object
654
c0c8e1c6 655validate_object takes the context and the object as an argument. It then filters
656the passed values in slot two of the tuple through the create|update_allows
657configured. It then returns those filtered values. Values that are not allowed
658are silently ignored. If there are no values for a particular key, no valid
659values at all, or multiple of the same key, this method will die.
d2739840 660
661=cut
662
8ea592cb 663sub validate_object {
664 my ( $self, $c, $obj ) = @_;
665 my ( $object, $params ) = @$obj;
d2739840 666
667 my %values;
8ea592cb 668 my %requires_map = map { $_ => 1 } @{
669 ( $object->in_storage )
406086f3 670 ? []
d2739840 671 : $c->stash->{create_requires} || $self->create_requires
672 };
406086f3 673
8ea592cb 674 my %allows_map = map { ( ref $_ ) ? %{$_} : ( $_ => 1 ) } (
406086f3 675 keys %requires_map,
8ea592cb 676 @{ ( $object->in_storage )
677 ? ( $c->stash->{update_allows} || $self->update_allows )
678 : ( $c->stash->{create_allows} || $self->create_allows )
d2739840 679 }
680 );
681
8ea592cb 682 foreach my $key ( keys %allows_map ) {
683
d2739840 684 # check value defined if key required
685 my $allowed_fields = $allows_map{$key};
406086f3 686
8ea592cb 687 if ( ref $allowed_fields ) {
d2739840 688 my $related_source = $object->result_source->related_source($key);
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 752delete operates on the stored objects in the request. It first transacts the
753objects, deleting them in the database using L</transact_objects> and a closure
754around L</delete_objects>, and then clears the request store of objects.
d2739840 755
756=cut
757
8ea592cb 758sub 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 776This method is used by update_or_create to perform the actual database
777manipulations. It iterates each object calling L</save_object>.
d2739840 778
b421ef50 779=cut
780
8ea592cb 781sub 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 791save_object first checks to see if the object is already in storage. If so, it
792calls L</update_object_from_params> otherwise L</insert_object_from_params>.
d2739840 793
794=cut
795
8ea592cb 796sub 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 812update_object_from_params iterates through the params to see if any of them are
813pertinent to relations. If so it calls L</update_object_relation> with the
814object, and the relation parameters. Then it calls ->update on the object.
b421ef50 815
816=cut
817
8ea592cb 818sub 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 846update_object_relation finds the relation to the object, then calls ->update
847with the specified parameters.
b421ef50 848
849=cut
850
8ea592cb 851sub 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 894Sets the columns of the object, then calls ->insert.
b421ef50 895
896=cut
897
8ea592cb 898sub 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
933Attempts to find the related row by introspecting the result source and determining
934if we have enough data to properly update.
935
936=cut
937
938sub 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 966Iterates through each object calling L</delete_object>.
b421ef50 967
968=cut
969
8ea592cb 970sub 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 978Performs the actual ->delete on the object.
b421ef50 979
980=cut
981
8ea592cb 982sub 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 992end performs the final manipulation of the response before it is serialized.
993This includes setting the success of the request both at the HTTP layer and
994JSON layer. If configured with return_object true, and there are stored objects
995as the result of create or update, those will be inflated according to the
996schema and get_inflated_columns
d2739840 997
998=cut
999
8ea592cb 1000sub 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 1041each_object_inflate executes during L</end> and allows hooking into the process
1042of inflating the objects to return in the response. Receives, the context, and
1043the object as arguments.
c9b8a798 1044
c0c8e1c6 1045This only executes if L</return_object> if set and if there are any objects to
1046actually return.
c9b8a798 1047
1048=cut
1049
8ea592cb 1050sub 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
1060multiple actions forward to serialize which uses Catalyst::Action::Serialize.
1061
1062=cut
1063
c9b8a798 1064# from Catalyst::Action::Serialize
8ea592cb 1065sub serialize : ActionClass('Serialize') { }
c9b8a798 1066
d2739840 1067=method_protected push_error
1068
c0c8e1c6 1069Stores an error message into the stash to be later retrieved by L</end>.
1070Accepts a Dict[message => Str] parameter that defines the error message.
d2739840 1071
1072=cut
1073
8ea592cb 1074sub 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 1091Returns all of the errors stored in the stash.
d2739840 1092
1093=cut
1094
8ea592cb 1095sub 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 1104Returns true if errors are stored in the stash.
71c17090 1105
1106=cut
1107
1108sub 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 1117Easily provide common API endpoints based on your L<DBIx::Class> schema classes.
1118Module provides both RPC and REST interfaces to base functionality.
1119Uses L<Catalyst::Action::Serialize> and L<Catalyst::Action::Deserialize> to
1120serialize response and/or deserialise request.
d2739840 1121
1122=head1 OVERVIEW
1123
c0c8e1c6 1124This document describes base functionlity such as list, create, delete, update
1125and the setting of config attributes. L<Catalyst::Controller::DBIC::API::RPC>
1126and L<Catalyst::Controller::DBIC::API::REST> describe details of provided
1127endpoints to those base methods.
d2739840 1128
c0c8e1c6 1129You will need to create a controller for each schema class you require API
1130endpoints for. For example if your schema has Artist and Track, and you want to
1131provide a RESTful interface to these, you should create
1132MyApp::Controller::API::REST::Artist and MyApp::Controller::API::REST::Track
1133which both subclass L<Catalyst::Controller::DBIC::API::REST>.
1134Similarly if you wanted to provide an RPC style interface then subclass
1135L<Catalyst::Controller::DBIC::API::RPC>. You then configure these individually
1136as specified in L</CONFIGURATION>.
d2739840 1137
c0c8e1c6 1138Also note that the test suite of this module has an example application used to
1139run tests against. It maybe helpful to look at that until a better tutorial is
1140written.
d2739840 1141
1142=head2 CONFIGURATION
1143
c0c8e1c6 1144Each of your controller classes needs to be configured to point at the relevant
1145schema class, specify what can be updated and so on, as shown in the L</SYNOPSIS>.
d2739840 1146
c0c8e1c6 1147The class, create_requires, create_allows and update_requires parameters can
1148also 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 1165Generally it's better to have one controller for each DBIC source with the
1166config hardcoded, but in some cases this isn't possible.
d2739840 1167
c0c8e1c6 1168Note that the Chained, CaptureArgs and PathPart are just standard Catalyst
1169configuration parameters and that then endpoint specified in Chained - in this
1170case '/api/rpc/rpc_base' - must actually exist elsewhere in your application.
1171See L<Catalyst::DispatchType::Chained> for more details.
d2739840 1172
c0c8e1c6 1173Below are explanations for various configuration parameters. Please see
1174L<Catalyst::Controller::DBIC::API::StaticArguments> for more details.
d2739840 1175
1176=head3 class
1177
c0c8e1c6 1178Whatever you would pass to $c->model to get a resultset for this class.
1179MyAppDB::Track for example.
d2739840 1180
a0a4ed30 1181=head3 resultset_class
1182
c0c8e1c6 1183Desired resultset class after accessing your model. MyAppDB::ResultSet::Track
1184for example. By default, it's DBIx::Class::ResultClass::HashRefInflator.
1185Set to empty string to leave resultset class without change.
a0a4ed30 1186
810de6af 1187=head3 stash_key
1188
1189Controls where in stash request_data should be stored, and defaults to 'response'.
1190
d2739840 1191=head3 data_root
1192
c0c8e1c6 1193By 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
1196the needs of the consuming client.
d2739840 1197
1198=head3 use_json_boolean
1199
c0c8e1c6 1200By 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
1202instead. 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 1206These attributes allow customization of the component to understand requests
1207made by clients where these argument names are not flexible and cannot conform
1208to this components defaults.
d2739840 1209
1210=head3 create_requires
1211
c0c8e1c6 1212Arrayref listing columns required to be passed to create in order for the
1213request to be valid.
d2739840 1214
1215=head3 create_allows
1216
c0c8e1c6 1217Arrayref listing columns additional to those specified in create_requires that
1218are not required to create but which create does allow. Columns passed to create
1219that are not listed in create_allows or create_requires will be ignored.
d2739840 1220
1221=head3 update_allows
1222
c0c8e1c6 1223Arrayref listing columns that update will allow. Columns passed to update that
1224are not listed here will be ignored.
d2739840 1225
1226=head3 select
1227
c0c8e1c6 1228Arguments to pass to L<DBIx::Class::ResultSet/select> when performing search for
1229L</list>.
d2739840 1230
1231=head3 as
1232
c0c8e1c6 1233Complements arguments passed to L<DBIx::Class::ResultSet/select> when performing
1234a search. This allows you to specify column names in the result for RDBMS
1235functions, etc.
d2739840 1236
1237=head3 select_exposes
1238
c0c8e1c6 1239Columns and related columns that are okay to return in the resultset since
1240clients can request more or less information specified than the above select
1241argument.
d2739840 1242
1243=head3 prefetch
1244
c0c8e1c6 1245Arguments to pass to L<DBIx::Class::ResultSet/prefetch> when performing search
1246for L</list>.
d2739840 1247
1248=head3 prefetch_allows
1249
1250Arrayref listing relationships that are allowed to be prefetched.
1251This is necessary to avoid denial of service attacks in form of
1252queries which would return a large number of data
1253and unwanted disclosure of data.
1254
1255=head3 grouped_by
1256
c0c8e1c6 1257Arguments to pass to L<DBIx::Class::ResultSet/group_by> when performing search
1258for L</list>.
d2739840 1259
1260=head3 ordered_by
1261
c0c8e1c6 1262Arguments to pass to L<DBIx::Class::ResultSet/order_by> when performing search
1263for L</list>.
d2739840 1264
1265=head3 search_exposes
1266
c0c8e1c6 1267Columns and related columns that are okay to search on. For example if only the
1268position column and all cd columns were to be allowed
d2739840 1269
1270 search_exposes => [qw/position/, { cd => ['*'] }]
1271
c0c8e1c6 1272You can also use this to allow custom columns should you wish to allow them
1273through 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
1284and 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 1303Arguments to pass to L<DBIx::Class::ResultSet/rows> when performing search for
1304L</list>.
d2739840 1305
1306=head3 page
1307
c0c8e1c6 1308Arguments to pass to L<DBIx::Class::ResultSet/page> when performing search for
1309L</list>.
d2739840 1310
1311=head1 EXTENDING
1312
c0c8e1c6 1313By default the create, delete and update actions will not return anything apart
1314from the success parameter set in L</end>, often this is not ideal but the
1315required behaviour varies from application to application. So normally it's
1316sensible to write an intermediate class which your main controller classes
1317subclass from.
d2739840 1318
c0c8e1c6 1319For example if you wanted create to return the JSON for the newly created
1320object 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 1345It should be noted that the return_object attribute will produce the above
1346result for you, free of charge.
d2739840 1347
c0c8e1c6 1348Similarly you might want create, update and delete to all forward to the list
1349action once they are done so you can refresh your view. This should also be
1350simple enough.
d2739840 1351
c0c8e1c6 1352If more extensive customization is required, it is recommened to peer into the
1353roles that comprise the system and make use
d2739840 1354
1355=head1 NOTES
1356
c0c8e1c6 1357It should be noted that version 1.004 and above makes a rapid depature from the
1358status quo. The internals were revamped to use more modern tools such as Moose
1359and its role system to refactor functionality out into self-contained roles.
1360
1361To this end, internally, this module now understands JSON boolean values (as
1362represented by the JSON module) and will Do The Right Thing in handling those
1363values. This means you can have ColumnInflators installed that can covert
1364between JSON booleans and whatever your database wants for boolean values.
d2739840 1365
c0c8e1c6 1366Validation for various *_allows or *_exposes is now accomplished via
1367Data::DPath::Validator with a lightly simplified, via a subclass of
1368Data::DPath::Validator::Visitor.
d2739840 1369
c0c8e1c6 1370The rough jist of the process goes as follows: Arguments provided to those
1371attributes are fed into the Validator and Data::DPaths are generated.
1372Then incoming requests are validated against these paths generated.
1373The validator is set in "loose" mode meaning only one path is required to match.
1374For more information, please see L<Data::DPath::Validator> and more specifically
1375L<Catalyst::Controller::DBIC::API::Validator>.
d2739840 1376
1377Since 2.00100:
c0c8e1c6 1378Transactions are used. The stash is put aside in favor of roles applied to the
1379request object with additional accessors.
d2739840 1380Error handling is now much more consistent with most errors immediately detaching.
1381The internals are much easier to read and understand with lots more documentation.
1382
1383=cut
1384
13851;