File Coverage

lib/Net/Amazon/DynamoDB.pm
Criterion Covered Total %
statement 1 3 33.3
branch n/a
condition n/a
subroutine 1 1 100.0
pod n/a
total 2 4 50.0


line stmt bran cond sub pod time code
1             package Net::Amazon::DynamoDB;
2              
3             =head1 NAME
4              
5             Net::Amazon::DynamoDB - Simple interface for Amazon DynamoDB
6              
7             =head1 DESCRIPTION
8              
9             Simple to use interface for Amazon DynamoDB
10              
11             If you want an ORM-like interface with real objects to work with, this is implementation is not for you. If you just want to access DynamoDB in a simple/quick manner - you are welcome.
12              
13             See L<https://github.com/ukautz/Net-Amazon-DynamoDB> for latest release.
14              
15             =head1 SYNOPSIS
16              
17             my $ddb = Net::Amazon::DynamoDB->new(
18             access_key => $my_access_key,
19             secret_key => $my_secret_key,
20             tables => {
21              
22             # table with only hash key
23             sometable => {
24             hash_key => 'id',
25             attributes => {
26             id => 'N',
27             name => 'S'
28             }
29             },
30              
31             # table with hash and reange key key
32             othertable => {
33             hash_key => 'id',
34             range_key => 'range_id',
35             attributes => {
36             id => 'N',
37             range_id => 'N',
38             attrib1 => 'S',
39             attrib2 => 'S'
40             }
41             }
42             }
43             );
44              
45             # create both tables with 10 read and 5 write unites
46             $ddb->exists_table( $_ ) || $ddb->create_table( $_, 10, 5 )
47             for qw/ sometable othertable /;
48              
49             # insert something into tables
50             $ddb->put_item( sometable => {
51             id => 5,
52             name => 'bla'
53             } ) or die $ddb->error;
54             $ddb->put_item( sometable => {
55             id => 5,
56             range_id => 7,
57             attrib1 => 'It is now '. localtime(),
58             attrib2 => 'Or in unix timstamp '. time(),
59             } ) or die $ddb->error;
60              
61             =cut
62              
63 1     1   1390 use Moose;
  0            
  0            
64              
65             use v5.10;
66             use version 0.74; our $VERSION = qv( "v0.1.16" );
67              
68             use Carp qw/ croak /;
69             use Data::Dumper;
70             use DateTime::Format::HTTP;
71             use DateTime::Format::Strptime;
72             use DateTime;
73             use Digest::SHA qw/ sha1_hex sha256_hex sha384_hex sha256 hmac_sha256_base64 /;
74             use HTTP::Request;
75             use JSON;
76             use LWP::UserAgent;
77             use LWP::ConnCache;
78             use Net::Amazon::AWSSign;
79             use Time::HiRes qw/ usleep /;
80             use XML::Simple qw/ XMLin /;
81             use Encode;
82              
83             =head1 CLASS ATTRIBUTES
84              
85             =head2 tables
86              
87             The table definitions
88              
89             =cut
90              
91             has tables => ( isa => 'HashRef[HashRef]', is => 'rw', required => 1, trigger => sub {
92             my ( $self ) = @_;
93              
94             # check table
95             while( my ( $table, $table_ref ) = each %{ $self->{ tables } } ) {
96              
97             # determine primary keys
98             my @check_pk = ( 'hash' );
99             push @check_pk, 'range'
100             if defined $table_ref->{ range_key };
101              
102             # check primary keys
103             foreach my $check_pk( @check_pk ) {
104             my $key_pk = "${check_pk}_key";
105             my $name_pk = $table_ref->{ $key_pk };
106             croak "Missing '$key_pk' attribute in '$table' table definition\n"
107             unless defined $table_ref->{ $key_pk };
108             croak "Missing $check_pk key attribute in '$table' table attribute declaration: "
109             . "{ $table => { attributes => { '$name_pk' => 'S|N' } }\n"
110             unless defined $table_ref->{ attributes }->{ $name_pk };
111             croak "Wrong data type for $check_pk key attribute. Got '$table_ref->{ attributes }->{ $name_pk }',"
112             . " expect 'S' or 'N'"
113             unless $table_ref->{ attributes }->{ $name_pk } =~ /^(S|N)$/;
114             }
115              
116             # check attributes
117             while( my( $attr_name, $attr_type ) = each %{ $table_ref->{ attributes } } ) {
118             croak "Wrong data type for attribute '$attr_name' in table '$table': Got '$attr_type' was"
119             . " expecting 'S' or 'N' or 'SS' or 'NS'"
120             unless $attr_type =~ /^[NS]S?$/;
121             }
122             }
123              
124             # no need to go further, if no namespace given
125             return unless $self->namespace;
126              
127             # update table definitions with namespace
128             my %new_table = ();
129             my $updated = 0;
130             foreach my $table( keys %{ $self->{ tables } } ) {
131             my $table_updated = index( $table, $self->namespace ) == 0 ? $table : $self->_table_name( $table );
132             $new_table{ $table_updated } = $self->{ tables }->{ $table };
133             $updated ++ unless $table_updated eq $table;
134             }
135             if ( $updated ) {
136             $self->{ tables } = \%new_table;
137             }
138             } );
139              
140             =head2 use_keep_alive
141              
142             Use keep_alive connections to AWS (Uses C<LWP::ConnCache> experimental mechanism). 0 to disable, positive number sets value for C<LWP::UserAgent> attribute 'keep_alive'
143             Default: 0
144              
145             =cut
146              
147             has use_keep_alive => ( isa => 'Int', is => 'rw', default => 0 );
148              
149             =head2 lwp
150              
151             Contains C<LWP::UserAgent> instance.
152              
153             =cut
154              
155             has lwp => ( isa => 'LWP::UserAgent', is => 'rw', lazy => 1, default => sub { my ($self) = @_; LWP::UserAgent->new( timeout => 5, keep_alive => $self->use_keep_alive ) } );
156             has _lwpcache => ( isa => 'LWP::ConnCache', is => 'ro', lazy => 1, default => sub { my ($self) = @_; $self->lwp->conn_cache(); } );
157              
158             =head2 json
159              
160             Contains C<JSON> instance for decoding/encoding json.
161              
162             JSON object needs to support: canonical, allow_nonref and utf8
163              
164             =cut
165              
166             has json => ( isa => 'JSON', is => 'rw', default => sub { JSON->new()->canonical( 1 )->allow_nonref( 1 )->utf8( 1 ) }, trigger => sub {
167             shift->json->canonical( 1 )->allow_nonref( 1 )->utf8( 1 );
168             } );
169              
170             =head2 host
171              
172             DynamoDB API Hostname
173              
174             Default: dynamodb.us-east-1.amazonaws.com
175              
176             =cut
177              
178             has host => ( isa => 'Str', is => 'rw', default => 'dynamodb.us-east-1.amazonaws.com' );
179              
180             =head2 access_key
181              
182             AWS API access key
183              
184             Required!
185              
186             =cut
187              
188             has access_key => ( isa => 'Str', is => 'rw', required => 1 );
189              
190             =head2 secret_key
191              
192             AWS API secret key
193              
194             Required!
195              
196             =cut
197              
198             has secret_key => ( isa => 'Str', is => 'rw', required => 1 );
199              
200             =head2 api_version
201              
202             AWS API Version. Use format "YYYYMMDD"
203              
204             Default: 20111205
205              
206             =cut
207              
208             has api_version => ( isa => 'Str', is => 'rw', default => '20111205' );
209              
210             =head2 read_consistent
211              
212             Whether reads (get_item, batch_get_item) consistent per default or not. This does not affect scan_items or query_items, which are always eventually consistent.
213              
214             Default: 0 (eventually consistent)
215              
216             =cut
217              
218             has read_consistent => ( isa => 'Bool', is => 'rw', default => 0 );
219              
220             =head2 namespace
221              
222             Table prefix, prepended before table name on usage
223              
224             Default: ''
225              
226             =cut
227              
228             has namespace => ( isa => 'Str', is => 'ro', default => '' );
229              
230             =head2 raise_error
231              
232             Whether database errors (eg 4xx Response from DynamoDB) raise errors or not.
233              
234             Default: 0
235              
236             =cut
237              
238             has raise_error => ( isa => 'Bool', is => 'rw', default => 0 );
239              
240             =head2 max_retries
241              
242             Amount of retries a query will be tries if ProvisionedThroughputExceededException is raised until final error.
243              
244             Default: 0 (do only once, no retries)
245              
246             =cut
247              
248             has max_retries => ( isa => 'Int', is => 'rw', default => 1 );
249              
250             =head2 derive_table
251              
252             Whether we parse results using table definition (faster) or without a known definition (still requires table definition for indexes)
253              
254             Default: 0
255              
256             =cut
257              
258             has derive_table => ( isa => 'Bool', is => 'rw', default => 0 );
259              
260             =head2 retry_timeout
261              
262             Wait period in seconds between tries. Float allowed.
263              
264             Default: 0.1 (100ms)
265              
266             =cut
267              
268             has retry_timeout => ( isa => 'Num', is => 'rw', default => 0.1 );
269              
270             =head2 cache
271              
272             Cache object using L<Cache> interface, eg L<Cache::File> or L<Cache::Memcached>
273              
274             If set, caching is used for get_item, put_item, update_item and batch_get_item.
275              
276             Default: -
277              
278             =cut
279              
280             has cache => ( isa => 'Cache', is => 'rw', predicate => 'has_cache' );
281              
282             =head2 cache_disabled
283              
284             If cache is set, you still can disable it per default and enable it per operation with "use_cache" option (see method documentation)
285             This way you have a default no-cache policy, but still can use cache in choosen operations.
286              
287             Default: 0
288              
289             =cut
290              
291             has cache_disabled => ( isa => 'Bool', is => 'rw', default => 0 );
292              
293             =head2 cache_key_method
294              
295             Which one to use. Either sha1_hex, sha256_hex, sha384_hex or coderef
296              
297             Default: sha1_hex
298              
299             =cut
300              
301             has cache_key_method => ( is => 'rw', default => sub { \&Digest::SHA::sha1_hex }, trigger => sub {
302             my ( $self, $method ) = @_;
303             if ( ( ref( $method ) ) ne 'CODE' ) {
304             if ( $method eq 'sha1_hex' ) {
305             $self->{ cache_key_method } = \&Digest::SHA::sha1_hex();
306             }
307             elsif ( $method eq 'sha256_hex' ) {
308             $self->{ cache_key_method } = \&Digest::SHA::sha256_hex();
309             }
310             elsif ( $method eq 'sha384_hex' ) {
311             $self->{ cache_key_method } = \&Digest::SHA::sha384_hex();
312             }
313             }
314             } );
315              
316             #
317             # _aws_signer
318             # Contains C<Net::Amazon::AWSSign> instance.
319             #
320              
321             has _aws_signer => ( isa => 'Net::Amazon::AWSSign', is => 'rw', predicate => '_has_aws_signer' );
322              
323             #
324             # _security_token_url
325             # URL for receiving security token
326             #
327              
328             has _security_token_url => ( isa => 'Str', is => 'rw', default => 'https://sts.amazonaws.com/?Action=GetSessionToken&Version=2011-06-15' );
329              
330             #
331             # _credentials
332             # Contains credentials received by GetSession
333             #
334              
335             has _credentials => ( isa => 'HashRef[Str]', is => 'rw', predicate => '_has_credentials' );
336              
337             #
338             # _credentials_expire
339             # Time of credentials exiration
340             #
341              
342             has _credentials_expire => ( isa => 'DateTime', is => 'rw' );
343              
344             #
345             # _error
346             # Contains credentials received by GetSession
347             #
348              
349             has _error => ( isa => 'Str', is => 'rw', predicate => '_has_error' );
350              
351             =head1 METHODS
352              
353              
354             =head2 create_table $table_name, $read_amount, $write_amount
355              
356             Create a new Table. Returns description of the table
357              
358             my $desc_ref = $ddb->create_table( 'table_name', 10, 5 )
359             $desc_ref = {
360             count => 123, # amount of "rows"
361             status => 'CREATING', # or 'ACTIVE' or 'UPDATING' or some error state?
362             created => 1328893776, # timestamp
363             read_amount => 10, # amount of read units
364             write_amount => 5, # amount of write units
365             hash_key => 'id', # name of the hash key attribute
366             hash_key_type => 'S', # or 'N',
367             #range_key => 'id', # name of the hash key attribute (optional)
368             #range_key_type => 'S', # or 'N' (optional)
369             }
370              
371             =cut
372              
373             sub create_table {
374             my ( $self, $table, $read_amount, $write_amount ) = @_;
375             $table = $self->_table_name( $table );
376             $read_amount ||= 10;
377             $write_amount ||= 5;
378              
379             # check & get table definition
380             my $table_ref = $self->_check_table( "create_table", $table );
381              
382             # init create definition
383             my %create = (
384             TableName => $table,
385             ProvisionedThroughput => {
386             ReadCapacityUnits => $read_amount + 0,
387             WriteCapacityUnits => $write_amount + 0,
388             }
389             );
390              
391             # build keys
392             $create{ KeySchema } = {
393             HashKeyElement => {
394             AttributeName => $table_ref->{ hash_key },
395             AttributeType => $table_ref->{ attributes }->{ $table_ref->{ hash_key } }
396             }
397             };
398             if ( defined $table_ref->{ range_key } ) {
399             $create{ KeySchema }->{ RangeKeyElement } = {
400             AttributeName => $table_ref->{ range_key },
401             AttributeType => $table_ref->{ attributes }->{ $table_ref->{ range_key } }
402             };
403             }
404              
405             # perform create
406             my ( $res, $res_ok, $json_ref ) = $self->request( CreateTable => \%create );
407              
408             # got res
409             if ( $res_ok && defined $json_ref->{ TableDescription } ) {
410             return {
411             status => $json_ref->{ TableDescription }->{ TableStatus },
412             created => int( $json_ref->{ TableDescription }->{ CreationDateTime } ),
413             read_amount => $json_ref->{ TableDescription }->{ ProvisionedThroughput }->{ ReadCapacityUnits },
414             write_amount => $json_ref->{ TableDescription }->{ ProvisionedThroughput }->{ WriteCapacityUnits },
415             hash_key => $json_ref->{ Table }->{ KeySchema }->{ HashKeyElement }->{ AttributeName },
416             hash_key_type => $json_ref->{ Table }->{ KeySchema }->{ HashKeyElement }->{ AttributeType },
417             ( defined $json_ref->{ Table }->{ KeySchema }->{ RangeKeyElement }
418             ? (
419             range_key => $json_ref->{ Table }->{ KeySchema }->{ RangeKeyElement }->{ AttributeName },
420             range_key_type => $json_ref->{ Table }->{ KeySchema }->{ RangeKeyElement }->{ AttributeType },
421             )
422             : ()
423             ),
424             }
425             }
426              
427             # set error
428             $self->error( 'create_table failed: '. $self->_extract_error_message( $res ) );
429             return ;
430             }
431              
432              
433              
434             =head2 delete_table $table
435              
436             Delete an existing (and defined) table.
437              
438             Returns bool whether table is now in deleting state (succesfully performed)
439              
440             =cut
441              
442             sub delete_table {
443             my ( $self, $table ) = @_;
444             $table = $self->_table_name( $table );
445              
446             # check & get table definition
447             my $table_ref = $self->_check_table( delete_table => $table );
448              
449             # perform create
450             my ( $res, $res_ok, $json_ref ) = $self->request( DeleteTable => { TableName => $table } );
451              
452             # got result
453             if ( $res_ok && defined $json_ref->{ TableDescription } ) {
454             return $json_ref->{ TableDescription }->{ TableStatus } eq 'DELETING';
455             }
456              
457             # set error
458             $self->error( 'delete_table failed: '. $self->_extract_error_message( $res ) );
459             return ;
460             }
461              
462              
463              
464             =head2 describe_table $table
465              
466             Returns table information
467              
468             my $desc_ref = $ddb->describe_table( 'my_table' );
469             $desc_ref = {
470             existing => 1,
471             size => 123213, # data size in bytes
472             count => 123, # amount of "rows"
473             status => 'ACTIVE', # or 'DELETING' or 'CREATING' or 'UPDATING' or some error state
474             created => 1328893776, # timestamp
475             read_amount => 10, # amount of read units
476             write_amount => 5, # amount of write units
477             hash_key => 'id', # name of the hash key attribute
478             hash_key_type => 'S', # or 'N',
479             #range_key => 'id', # name of the hash key attribute (optional)
480             #range_key_type => 'S', # or 'N' (optional)
481             }
482              
483             If no such table exists, return is
484              
485             {
486             existing => 0
487             }
488              
489             =cut
490              
491             sub describe_table {
492             my ( $self, $table ) = @_;
493             $table = $self->_table_name( $table );
494              
495             # check table definition
496             $self->_check_table( "describe_table", $table );
497              
498             my ( $res, $res_ok, $json_ref ) = $self->request( DescribeTable => { TableName => $table } );
499             # got result
500             if ( $res_ok ) {
501             if ( defined $json_ref->{ Table } ) {
502             no warnings 'uninitialized';
503             return {
504             existing => 1,
505             size => $json_ref->{ Table }->{ TableSizeBytes },
506             count => $json_ref->{ Table }->{ ItemCount },
507             status => $json_ref->{ Table }->{ TableStatus },
508             created => int( $json_ref->{ Table }->{ CreationDateTime } ),
509             read_amount => $json_ref->{ Table }->{ ProvisionedThroughput }->{ ReadCapacityUnits },
510             write_amount => $json_ref->{ Table }->{ ProvisionedThroughput }->{ WriteCapacityUnits },
511             hash_key => $json_ref->{ Table }->{ KeySchema }->{ HashKeyElement }->{ AttributeName },
512             hash_key_type => $json_ref->{ Table }->{ KeySchema }->{ HashKeyElement }->{ AttributeType },
513             ( defined $json_ref->{ Table }->{ KeySchema }->{ RangeKeyElement }
514             ? (
515             range_key => $json_ref->{ Table }->{ KeySchema }->{ RangeKeyElement }->{ AttributeName },
516             range_key_type => $json_ref->{ Table }->{ KeySchema }->{ RangeKeyElement }->{ AttributeType },
517             )
518             : ()
519             ),
520             };
521             }
522             else {
523             return {
524             existing => 0
525             }
526             }
527             }
528              
529             # set error
530             $self->error( 'describe_table failed: '. $self->_extract_error_message( $res ) );
531             return ;
532             }
533              
534              
535             =head2 update_table $table, $read_amount, $write_amount
536              
537             Update read and write amount for a table
538              
539             =cut
540              
541             sub update_table {
542             my ( $self, $table, $read_amount, $write_amount ) = @_;
543             $table = $self->_table_name( $table );
544              
545             my ( $res, $res_ok, $json_ref ) = $self->request( UpdateTable => {
546             TableName => $table,
547             ProvisionedThroughput => {
548             ReadCapacityUnits => $read_amount + 0,
549             WriteCapacityUnits => $write_amount + 0,
550             }
551             } );
552              
553             if ( $res_ok ) {
554             return 1;
555             }
556              
557             # set error
558             $self->error( 'update_table failed: '. $self->_extract_error_message( $res ) );
559             return ;
560             }
561              
562              
563              
564             =head2 exists_table $table
565              
566             Returns bool whether table exists or not
567              
568             =cut
569              
570             sub exists_table {
571             my ( $self, $table ) = @_;
572             $table = $self->_table_name( $table );
573              
574             # check table definition
575             $self->_check_table( "exists_table", $table );
576              
577             my ( $res, $res_ok, $json_ref );
578             eval {
579             ( $res, $res_ok, $json_ref ) = $self->request( DescribeTable => { TableName => $table } );
580             };
581              
582             return defined $json_ref->{ Table } && defined $json_ref->{ Table }->{ ItemCount } ? 1 : 0
583             if $res_ok;
584              
585             # set error
586             return 0;
587             }
588              
589              
590              
591             =head2 list_tables
592              
593             Returns tables names as arrayref (or array in array context)
594              
595             =cut
596              
597             sub list_tables {
598             my ( $self ) = @_;
599              
600             my ( $res, $res_ok, $json_ref ) = $self->request( ListTables => {} );
601             if ( $res_ok ) {
602             my $ns_length = length( $self->namespace );
603             my @table_names = map {
604             substr( $_, $ns_length );
605             } grep {
606             ! $self->namespace || index( $_, $self->namespace ) == 0
607             } @{ $json_ref->{ TableNames } };
608             return wantarray ? @table_names : \@table_names;
609             }
610              
611             # set error
612             $self->error( 'list_tables failed: '. $self->_extract_error_message( $res ) );
613             return ;
614             }
615              
616              
617              
618             =head2 put_item $table, $item_ref, [$where_ref], [$args_ref]
619              
620             Write a single item to table. All primary keys are required in new item.
621              
622             # just write
623             $ddb->put_item( my_table => {
624             id => 123,
625             some_attrib => 'bla',
626             other_attrib => 'dunno'
627             } );
628              
629             # write conditionally
630             $ddb->put_item( my_table => {
631             id => 123,
632             some_attrib => 'bla',
633             other_attrib => 'dunno'
634             }, {
635             some_attrib => { # only update, if some_attrib has the value 'blub'
636             value => 'blub'
637             },
638             other_attrib => { # only update, if a value for other_attrib exists
639             exists => 1
640             }
641             } );
642              
643             =over
644              
645             =item * $table
646              
647             Name of the table
648              
649             =item * $item_ref
650              
651             Hashref containing the values to be inserted
652              
653             =item * $where_ref [optional]
654              
655             Filter containing expected values of the (existing) item to be updated
656              
657             =item * $args_ref [optional]
658              
659             HashRef with options
660              
661             =over
662              
663             =item * return_old
664              
665             If true, returns old value
666              
667             =item * no_cache
668              
669             Force not using cache, if enabled per default
670              
671             =item * use_cache
672              
673             Force using cache, if disabled per default but setupped
674              
675             =back
676              
677             =back
678              
679             =cut
680              
681             sub put_item {
682             my ( $self, $table, $item_ref, $where_ref, $args_ref ) = @_;
683             $args_ref ||= {
684             return_old => 0,
685             no_cache => 0,
686             use_cache => 0,
687             max_retries => undef
688             };
689             $table = $self->_table_name( $table );
690              
691             # check definition
692             my $table_ref = $self->_check_table( "put_item", $table );
693              
694             # check primary keys
695             croak "put_item: Missing value for hash key '$table_ref->{ hash_key }'"
696             unless defined $item_ref->{ $table_ref->{ hash_key } }
697             && length( $item_ref->{ $table_ref->{ hash_key } } );
698              
699             # check other attributes
700             $self->_check_keys( "put_item: item values", $table, $item_ref );
701              
702             # having where -> check now
703             $self->_check_keys( "put_item: where clause", $table, $where_ref ) if $where_ref;
704              
705             # build put
706             my %put = (
707             TableName => $table,
708             Item => {}
709             );
710              
711             # build the item
712             foreach my $key( keys %$item_ref ){
713             my $type = $self->_attrib_type( $table, $key );
714             my $value;
715             if ( $type eq 'SS' || $type eq 'NS' ) {
716             my @values = map { $_. '' } ( ref( $item_ref->{ $key } ) ? @{ $item_ref->{ $key } } : () );
717             $value = \@values;
718             }
719             else {
720             $value = $item_ref->{ $key } .'';
721             }
722             $put{ Item }->{ $key } = { $type => $value };
723             }
724              
725             # build possible where clause
726             if ( $where_ref ) {
727             $self->_build_attrib_filter( $table, $where_ref, $put{ Expected } = {} );
728             }
729              
730             # add return value, if set
731             $put{ ReturnValues } = 'ALL_OLD' if $args_ref->{ return_old };
732              
733             # perform create
734             my ( $res, $res_ok, $json_ref ) = $self->request( PutItem => \%put, {
735             max_retries => $args_ref->{ max_retries },
736             } );
737              
738             # get result
739             if ( $res_ok ) {
740              
741             # clear cache
742             if ( $self->_cache_enabled( $args_ref ) ) {
743             my $cache_key = $self->_cache_key_single( $table, $item_ref );
744             $self->cache->remove( $cache_key );
745             }
746              
747             if ( $args_ref->{ return_old } ) {
748             return defined $json_ref->{ Attributes }
749             ? $self->_format_item( $table, $json_ref->{ Attributes } )
750             : undef;
751             }
752             else {
753             return $json_ref->{ ConsumedCapacityUnits } > 0;
754             }
755             }
756              
757             # set error
758             $self->error( 'put_item failed: '. $self->_extract_error_message( $res ) );
759             return ;
760             }
761              
762              
763             =head2 batch_write_item $tables_ref, [$args_ref]
764              
765             Batch put / delete items into one ore more tables.
766              
767             Caution: Each batch put / delete cannot process more operations than you have write capacity for the table.
768              
769             Example:
770              
771             my ( $ok, $unprocessed_count, $next_query_ref ) = $ddb->batch_write_item( {
772             table_name => {
773             put => [
774             {
775             attrib1 => "Value 1",
776             attrib2 => "Value 2",
777             },
778             # { .. } ..
779             ],
780             delete => [
781             {
782             hash_key => "Hash Key Value",
783             range_key => "Range Key Value",
784             },
785             # { .. } ..
786             ]
787             },
788             # table2_name => ..
789             } );
790              
791             if ( $ok ) {
792             if ( $unprocessed_count ) {
793             print "Ok, but $unprocessed_count still not processed\n";
794             $ddb->batch_write_item( $next_query_ref );
795             }
796             else {
797             print "All processed\n";
798             }
799             }
800              
801             =over
802              
803             =item $tables_ref
804              
805             HashRef in the form
806              
807             { table_name => { put => [ { attribs }, .. ], delete => [ { primary keys } ] } }
808              
809             =item $args_ref
810              
811             HashRef
812              
813             =over
814              
815             =item * process_all
816              
817             Keep processing everything which is returned as unprocessed (if you send more operations than your
818             table has write capability or you surpass the max amount of operations OR max size of request (see AWS API docu)).
819              
820             Caution: Error handling
821              
822             Default: 0
823              
824             =back
825              
826             =back
827              
828             =cut
829              
830             sub batch_write_item {
831             my ( $self, $tables_ref, $args_ref ) = @_;
832             $args_ref ||= {
833             process_all => 0,
834             max_retries => undef
835             };
836              
837             # check definition
838             my %table_map;
839             foreach my $table( keys %$tables_ref ) {
840             $table = $self->_table_name( $table );
841             my $table_ref = $self->_check_table( "batch_write_item", $table );
842             $table_map{ $table } = $table_ref;
843             }
844              
845             my %write = ( RequestItems => {} );
846             foreach my $table( keys %table_map ) {
847             my $table_out = $self->_table_name( $table, 1 );
848             my $t_ref = $tables_ref->{ $table_out };
849             my $table_requests_ref = $write{ RequestItems }->{ $table } = [];
850              
851             foreach my $operation( qw/ put delete / ) {
852             next unless defined $t_ref->{ $operation };
853             my @operations = ref( $t_ref->{ $operation } ) eq 'ARRAY'
854             ? @{ $t_ref->{ $operation } }
855             : ( $t_ref->{ $operation } );
856              
857             # put ..
858             if ( $operation eq 'put' ) {
859             foreach my $put_ref( @operations ) {
860             push @$table_requests_ref, { 'PutRequest' => { Item => my $request_ref = {} } };
861              
862             # build the item
863             foreach my $key( keys %$put_ref ){
864             my $type = $self->_attrib_type( $table, $key );
865             my $value;
866             if ( $type eq 'SS' || $type eq 'NS' ) {
867             my @values = map { $_. '' } ( ref( $put_ref->{ $key } ) ? @{ $put_ref->{ $key } } : () );
868             $value = \@values;
869             }
870             else {
871             $value = $put_ref->{ $key } .'';
872             }
873             $request_ref->{ $key } = { $type => $value };
874             }
875             }
876             }
877              
878             # delete ..
879             else {
880             foreach my $delete_ref( @operations ) {
881             push @$table_requests_ref, { 'DeleteRequest' => { Key => my $request_ref = {} } };
882             $self->_build_pk_filter( $table, $delete_ref, $request_ref );
883             }
884             }
885             }
886             }
887              
888             # perform create
889             my ( $res, $res_ok, $json_ref ) = $self->request( BatchWriteItem => \%write, {
890             max_retries => $args_ref->{ max_retries },
891             } );
892              
893             # having more to process
894             while ( $args_ref->{ process_all }
895             && $res_ok
896             && defined $json_ref->{ UnprocessedItems }
897             && scalar( keys %{ $json_ref->{ UnprocessedItems } } )
898             ) {
899             ( $res, $res_ok, $json_ref ) = $self->request( BatchWriteItem => {
900             RequestItems => $json_ref->{ UnprocessedItems }
901             }, {
902             max_retries => $args_ref->{ max_retries },
903             } );
904             }
905              
906             # count unprocessed
907             my $unprocessed_count = 0;
908             my %next_query;
909             if ( $res_ok && defined $json_ref->{ UnprocessedItems } ) {
910             foreach my $table( keys %{ $json_ref->{ UnprocessedItems } } ) {
911             my @operations = @{ $json_ref->{ UnprocessedItems }->{ $table } };
912             next unless @operations;
913             $unprocessed_count += scalar( @operations );
914             $next_query{ $table } = {};
915             foreach my $operation_ref( @operations ) {
916             my ( $item_ref, $operation_name ) = defined $operation_ref->{ PutRequest }
917             ? ( $operation_ref->{ PutRequest }->{ Item }, 'put' )
918             : ( $operation_ref->{ DeleteRequest }->{ Key }, 'delete' );
919             #print Dumper( [ $operation_ref, $operation_name, $item_ref ] );
920             push @{ $next_query{ $table }->{ $operation_name } ||= [] },
921             $self->_format_item( $table, $item_ref )
922             }
923             }
924             }
925              
926             return wantarray ? ( $res_ok, $unprocessed_count, \%next_query ) : $res_ok;
927             }
928              
929              
930              
931             =head2 update_item $table, $update_ref, $where_ref, [$args_ref]
932              
933             Update existing item in database. All primary keys are required in where clause
934              
935             # update existing
936             $ddb->update_item( my_table => {
937             some_attrib => 'bla',
938             other_attrib => 'dunno'
939             }, {
940             id => 123,
941             } );
942              
943             # write conditionally
944             $ddb->update_item( my_table => {
945             some_attrib => 'bla',
946             other_attrib => 'dunno'
947             }, {
948             id => 123,
949             some_attrib => { # only update, if some_attrib has the value 'blub'
950             value => 'blub'
951             },
952             other_attrib => { # only update, if a value for other_attrib exists
953             exists => 1
954             }
955             } );
956              
957             =over
958              
959             =item * $table
960              
961             Name of the table
962              
963             =item * $update_ref
964              
965             Hashref containing the updates.
966              
967             =over
968              
969             =item * delete a single values
970              
971             { attribname => undef }
972              
973             =item * replace a values
974              
975             {
976             attribname1 => 'somevalue',
977             attribname2 => [ 1, 2, 3 ]
978             }
979              
980             =item * add values (arrays only)
981              
982             { attribname => \[ 4, 5, 6 ] }
983              
984             =back
985              
986             =item * $where_ref [optional]
987              
988             Filter HashRef
989              
990             =item * $args_ref [optional]
991              
992             HashRef of options
993              
994             =over
995              
996             =item * return_mode
997              
998             Can be set to on of "ALL_OLD", "UPDATED_OLD", "ALL_NEW", "UPDATED_NEW"
999              
1000             =item * no_cache
1001              
1002             Force not using cache, if enabled per default
1003              
1004             =item * use_cache
1005              
1006             Force using cache, if disabled per default but setupped
1007              
1008             =back
1009              
1010             =back
1011              
1012             =cut
1013              
1014             sub update_item {
1015             my ( $self, $table, $update_ref, $where_ref, $args_ref ) = @_;
1016             $args_ref ||= {
1017             return_mode => '',
1018             no_cache => 0,
1019             use_cache => 0,
1020             max_retries => undef
1021             };
1022             $table = $self->_table_name( $table );
1023              
1024             # check definition
1025             my $table_ref = $self->_check_table( "update_item", $table );
1026              
1027             croak "update_item: Cannot update hash key value, do not set it in update-clause"
1028             if defined $update_ref->{ $table_ref->{ hash_key } };
1029              
1030             croak "update_item: Cannot update range key value, do not set it in update-clause"
1031             if defined $table_ref->{ range_key }
1032             && defined $update_ref->{ $table_ref->{ range_key } };
1033              
1034             # check primary keys
1035             croak "update_item: Missing value for hash key '$table_ref->{ hash_key }' in where-clause"
1036             unless defined $where_ref->{ $table_ref->{ hash_key } }
1037             && length( $where_ref->{ $table_ref->{ hash_key } } );
1038             croak "update_item: Missing value for range key '$table_ref->{ hash_key }' in where-clause"
1039             if defined $table_ref->{ range_key } && !(
1040             defined $where_ref->{ $table_ref->{ range_key } }
1041             && length( $where_ref->{ $table_ref->{ range_key } } )
1042             );
1043              
1044             # check other attributes
1045             $self->_check_keys( "update_item: item values", $table, $update_ref );
1046             croak "update_item: Cannot update hash key '$table_ref->{ hash_key }'. You have to delete and put the item!"
1047             if defined $update_ref->{ $table_ref->{ hash_key } };
1048             croak "update_item: Cannot update range key '$table_ref->{ hash_key }'. You have to delete and put the item!"
1049             if defined $table_ref->{ range_key } && defined $update_ref->{ $table_ref->{ range_key } };
1050              
1051             # having where -> check now
1052             $self->_check_keys( "update_item: where clause", $table, $where_ref );
1053              
1054             # build put
1055             my %update = (
1056             TableName => $table,
1057             AttributeUpdates => {},
1058             Key => {}
1059             );
1060              
1061             # build the item
1062             foreach my $key( keys %$update_ref ) {
1063             my $type = $self->_attrib_type( $table, $key );
1064             my $value = $update_ref->{ $key };
1065              
1066             # delete
1067             if ( ! defined $value ) {
1068             $update{ AttributeUpdates }->{ $key } = {
1069             Action => 'DELETE'
1070             };
1071             }
1072              
1073             # if ++N or --N on numeric type, ADD to get inc/dec behavior
1074             elsif ( $type eq 'N' && $value =~ /^(--|\+\+)(\d+)$/ ) {
1075             $update{ AttributeUpdates }->{ $key } = {
1076             Value => { $type => ($1 eq '--') ? "-$2" : "$2" },
1077             Action => 'ADD'
1078             };
1079             }
1080              
1081             # replace for scalar
1082             elsif ( $type eq 'N' || $type eq 'S' ) {
1083             $update{ AttributeUpdates }->{ $key } = {
1084             Value => { $type => $value. '' },
1085             Action => 'PUT'
1086             };
1087             }
1088              
1089             # replace or add for array types
1090             elsif ( $type =~ /^[NS]S$/ ) {
1091              
1092             # add \[ qw/ value1 value2 / ]
1093             if ( ref( $value ) eq 'REF' ) {
1094             $update{ AttributeUpdates }->{ $key } = {
1095             Value => { $type => [ map { "$_" } @$$value ] },
1096             Action => 'ADD'
1097             };
1098             }
1099              
1100             # replace [ qw/ value1 value2 / ]
1101             else {
1102             $update{ AttributeUpdates }->{ $key } = {
1103             Value => { $type => [ map { "$_" } @$value ] },
1104             Action => 'PUT'
1105             };
1106             }
1107             }
1108             }
1109              
1110             # build possible where clause
1111             my %where = %$where_ref;
1112              
1113             # primary key
1114             $self->_build_pk_filter( $table, \%where, $update{ Key } );
1115              
1116             # additional filters
1117             if ( keys %where ) {
1118             $self->_build_attrib_filter( $table, \%where, $update{ Expected } = {} );
1119             }
1120              
1121             # add return value, if set
1122             if ( $args_ref->{ return_mode } ) {
1123             $update{ ReturnValues } = "$args_ref->{ return_mode }" =~ /^(?:ALL_OLD|UPDATED_OLD|ALL_NEW|UPDATED_NEW)$/i
1124             ? uc( $args_ref->{ return_mode } )
1125             : "ALL_OLD";
1126             }
1127              
1128             # perform create
1129             my ( $res, $res_ok, $json_ref ) = $self->request( UpdateItem => \%update, {
1130             max_retries => $args_ref->{ max_retries },
1131             } );
1132              
1133             # get result
1134             if ( $res_ok ) {
1135              
1136             # clear cache
1137             if ( $self->_cache_enabled( $args_ref ) ) {
1138             my $cache_key = $self->_cache_key_single( $table, $where_ref );
1139             $self->cache->remove( $cache_key );
1140             }
1141              
1142             if ( $args_ref->{ return_mode } ) {
1143             return defined $json_ref->{ Attributes }
1144             ? $self->_format_item( $table, $json_ref->{ Attributes } )
1145             : undef;
1146             }
1147             else {
1148             return $json_ref->{ ConsumedCapacityUnits } > 0;
1149             }
1150             }
1151              
1152             # set error
1153             $self->error( 'put_item failed: '. $self->_extract_error_message( $res ) );
1154             return ;
1155             }
1156              
1157              
1158              
1159             =head2 get_item $table, $pk_ref, [$args_ref]
1160              
1161             Read a single item by hash (and range) key.
1162              
1163             # only with hash key
1164             my $item1 = $ddb->get_item( my_table => { id => 123 } );
1165             print "Got $item1->{ some_key }\n";
1166              
1167             # with hash and range key, also consistent read and only certain attributes in return
1168             my $item2 = $ddb->get_item( my_other_table =>, {
1169             id => $hash_value, # the hash value
1170             title => $range_value # the range value
1171             }, {
1172             consistent => 1,
1173             attributes => [ qw/ attrib1 attrib2 ]
1174             } );
1175             print "Got $item2->{ attrib1 }\n";
1176              
1177             =over
1178              
1179             =item * $table
1180              
1181             Name of the table
1182              
1183             =item * $pk_ref
1184              
1185             HashRef containing all primary keys
1186              
1187             # only hash key
1188             {
1189             $hash_key => $hash_value
1190             }
1191              
1192             # hash and range key
1193             {
1194             $hash_key => $hash_value,
1195             $range_key => $range_value
1196             }
1197              
1198              
1199             =item * $args_ref [optional]
1200              
1201             HashRef of options
1202              
1203             =over
1204              
1205             =item * consistent
1206              
1207             Whether read shall be consistent. If set to 0 and read_consistent is globally enabled, this read will not be consistent
1208              
1209             =item * attributes
1210              
1211             ArrayRef of attributes to read. If not set, all attributes are returned.
1212              
1213             =item * no_cache
1214              
1215             Force not using cache, if enabled per default
1216              
1217             =item * use_cache
1218              
1219             Force using cache, if disabled per default but setupped
1220              
1221             =back
1222              
1223             =back
1224              
1225             =cut
1226              
1227             sub get_item {
1228             my ( $self, $table, $pk_ref, $args_ref ) = @_;
1229             $table = $self->_table_name( $table );
1230             $args_ref ||= {
1231             consistent => undef,
1232             attributes => undef,
1233             no_cache => 0,
1234             use_cache => 0,
1235             max_retries => undef
1236             };
1237             $args_ref->{ consistent } //= $self->read_consistent;
1238              
1239             # check definition
1240             my $table_ref = $self->_check_table( "get_item", $table );
1241              
1242             # check primary keys
1243             croak "get_item: Missing value for hash key '$table_ref->{ hash_key }'"
1244             unless defined $pk_ref->{ $table_ref->{ hash_key } }
1245             && length( $pk_ref->{ $table_ref->{ hash_key } } );
1246             croak "get_item: Missing value for Range Key '$table_ref->{ range_key }'"
1247             if defined $table_ref->{ range_key } && !(
1248             defined $pk_ref->{ $table_ref->{ range_key } }
1249             && length( $pk_ref->{ $table_ref->{ hash_key } } )
1250             );
1251              
1252             # use cache
1253             my $use_cache = $self->_cache_enabled( $args_ref );
1254             my $cache_key;
1255             if ( $use_cache ) {
1256             $cache_key = $self->_cache_key_single( $table, $pk_ref );
1257             my $cached = $self->cache->thaw( $cache_key );
1258             return $cached if defined $cached;
1259             }
1260              
1261              
1262             # build get
1263             my %get = (
1264             TableName => $table,
1265             ( defined $args_ref->{ attributes } ? ( AttributesToGet => $args_ref->{ attributes } ) : () ),
1266             ConsistentRead => $args_ref->{ consistent } ? \1 : \0,
1267             Key => {
1268             HashKeyElement => {
1269             $self->_attrib_type( $table, $table_ref->{ hash_key } ) =>
1270             $pk_ref->{ $table_ref->{ hash_key } }
1271             }
1272             }
1273             );
1274              
1275             # add range key ?
1276             if ( defined $table_ref->{ range_key } ) {
1277             $get{ Key }->{ RangeKeyElement } = {
1278             $self->_attrib_type( $table, $table_ref->{ range_key } ) =>
1279             $pk_ref->{ $table_ref->{ range_key } }
1280             };
1281             }
1282              
1283             # perform create
1284             my ( $res, $res_ok, $json_ref ) = $self->request( GetItem => \%get, {
1285             max_retries => $args_ref->{ max_retries },
1286             } );
1287              
1288             # return on success
1289             my $item_ref = $self->_format_item( $table, $json_ref->{ Item } ) if $res_ok && defined $json_ref->{ Item };
1290             if ( $use_cache ) {
1291             $self->cache->freeze( $cache_key, $item_ref ) if $item_ref;
1292             }
1293             return $item_ref;
1294              
1295             # return on success, but nothing received
1296             return undef if $res_ok;
1297              
1298             # set error
1299             $self->error( 'get_item failed: '. $self->_extract_error_message( $res ) );
1300             return ;
1301             }
1302              
1303              
1304              
1305             =head2 batch_get_item $tables_ref, [$args_ref]
1306              
1307             Read multiple items (possible accross multiple tables) identified by their hash and range key (if required).
1308              
1309             my $res = $ddb->batch_get_item( {
1310             table_name => [
1311             { $hash_key => $value1 },
1312             { $hash_key => $value2 },
1313             { $hash_key => $value3 },
1314             ],
1315             other_table_name => {
1316             keys => [
1317             { $hash_key => $value1, $range_key => $rvalue1 },
1318             { $hash_key => $value2, $range_key => $rvalue2 },
1319             { $hash_key => $value3, $range_key => $rvalue3 },
1320             ],
1321             attributes => [ qw/ attrib1 attrib2 / ]
1322             ]
1323             } );
1324              
1325             foreach my $table( keys %$res ) {
1326             foreach my $item( @{ $res->{ $table } } ) {
1327             print "$item->{ some_attrib }\n";
1328             }
1329             }
1330              
1331             =over
1332              
1333             =item $tables_ref
1334              
1335             HashRef of tablename => primary key ArrayRef
1336              
1337             =item $args_ref
1338              
1339             HashRef
1340              
1341             =over
1342              
1343             =item * process_all
1344              
1345             Batch request might not fetch all requested items at once. This switch enforces
1346             to batch get the unprocessed items.
1347              
1348             Default: 0
1349              
1350             =back
1351              
1352             =back
1353              
1354              
1355              
1356             =cut
1357              
1358             sub batch_get_item {
1359             my ( $self, $tables_ref, $args_ref ) = @_;
1360             $args_ref ||= {
1361             max_retries => undef,
1362             process_all => undef,
1363             consistent => undef
1364             };
1365             $args_ref->{ consistent } //= $self->read_consistent();
1366              
1367             # check definition
1368             my %table_map;
1369             foreach my $table( keys %$tables_ref ) {
1370             $table = $self->_table_name( $table );
1371             my $table_ref = $self->_check_table( "batch_get_item", $table );
1372             $table_map{ $table } = $table_ref;
1373             }
1374              
1375             my %get = ( RequestItems => {} );
1376             foreach my $table( keys %table_map ) {
1377             my $table_out = $self->_table_name( $table, 1 );
1378             my $t_ref = $tables_ref->{ $table_out };
1379              
1380             # init items for table
1381             $get{ RequestItems }->{ $table } = {};
1382              
1383             # init / get keys
1384             my $k_ref = $get{ RequestItems }->{ $table }->{ Keys } = [];
1385             my @keys = ref( $t_ref ) eq 'ARRAY'
1386             ? @$t_ref
1387             : @{ $t_ref->{ keys } };
1388              
1389             # get mapping for table
1390             my $m_ref = $table_map{ $table };
1391              
1392             # get hash key
1393             my $hash_key = $m_ref->{ hash_key };
1394             my $hash_key_type = $self->_attrib_type( $table, $hash_key );
1395              
1396             # get range key?
1397             my ( $range_key, $range_key_type );
1398             if ( defined $m_ref->{ range_key } ) {
1399             $range_key = $m_ref->{ range_key };
1400             $range_key_type = $self->_attrib_type( $table, $range_key );
1401             }
1402              
1403             # build request items
1404             foreach my $key_ref( @keys ) {
1405             push @$k_ref, {
1406             HashKeyElement => { $hash_key_type => $key_ref->{ $hash_key }. '' },
1407             ( defined $range_key ? ( RangeKeyElement => { $range_key_type => $key_ref->{ $range_key }. '' } ) : () )
1408             };
1409             }
1410              
1411             # having attributes limitation?
1412             if ( ref( $t_ref ) eq 'HASH' && defined $t_ref->{ attributes } ) {
1413             $get{ RequestItems }->{ $table }->{ AttributesToGet } = $t_ref->{ attributes };
1414             }
1415              
1416             # using consistent read?
1417             if ( $args_ref->{ consistent } ) {
1418             $get{ RequestItems }->{ $table }->{ ConsistentRead } = \1;
1419             }
1420             }
1421              
1422             # perform create
1423             my ( $res, $res_ok, $json_ref ) = $self->request( BatchGetItem => \%get, {
1424             max_retries => $args_ref->{ max_retries },
1425             } );
1426              
1427             # return on success
1428             if ( $res_ok && defined $json_ref->{ Responses } ) {
1429              
1430             if ( $args_ref->{ process_all } && defined( my $ukeys_ref = $json_ref->{ UnprocessedKeys } ) ) {
1431             while ( $ukeys_ref ) {
1432             ( $res, $res_ok, my $ujson_ref ) = $self->request( BatchGetItem =>
1433             {
1434             RequestItems => $ukeys_ref
1435             }, {
1436             max_retries => $args_ref->{ max_retries },
1437             } );
1438             if ( $res_ok && defined $ujson_ref->{ Responses } ) {
1439             foreach my $table_out( keys %$tables_ref ) {
1440             my $table = $self->_table_name( $table_out );
1441             if ( defined $ujson_ref->{ Responses }->{ $table } && defined $ujson_ref->{ Responses }->{ $table }->{ Items } ) {
1442             $json_ref->{ Responses }->{ $table } ||= {};
1443             push @{ $json_ref->{ Responses }->{ $table }->{ Items } ||= [] },
1444             @{ $ujson_ref->{ Responses }->{ $table }->{ Items } };
1445             }
1446             }
1447             }
1448             $ukeys_ref = $res_ok && defined $ujson_ref->{ UnprocessedKeys }
1449             ? $ujson_ref->{ UnprocessedKeys }
1450             : undef;
1451             }
1452             }
1453              
1454             my %res;
1455             foreach my $table_out( keys %$tables_ref ) {
1456             my $table = $self->_table_name( $table_out );
1457             next unless defined $json_ref->{ Responses }->{ $table } && defined $json_ref->{ Responses }->{ $table }->{ Items };
1458             my $items_ref = $json_ref->{ Responses }->{ $table };
1459             $res{ $table_out } = [];
1460             foreach my $item_ref( @{ $items_ref->{ Items } } ) {
1461             my %res_item;
1462             foreach my $attrib( keys %$item_ref ) {
1463             my $type = $self->_attrib_type( $table, $attrib );
1464             $res_item{ $attrib } = $item_ref->{ $attrib }->{ $type };
1465             }
1466             push @{ $res{ $table_out } }, \%res_item;
1467             }
1468             }
1469             return \%res;
1470             }
1471              
1472             # set error
1473             $self->error( 'batch_get_item failed: '. $self->_extract_error_message( $res ) );
1474             return ;
1475             }
1476              
1477              
1478              
1479             =head2 delete_item $table, $where_ref, [$args_ref]
1480              
1481             Deletes a single item by primary key (hash or hash+range key).
1482              
1483             # only with hash key
1484              
1485             =over
1486              
1487             =item * $table
1488              
1489             Name of the table
1490              
1491             =item * $where_ref
1492              
1493             HashRef containing at least primary key. Can also contain additional attribute filters
1494              
1495             =item * $args_ref [optional]
1496              
1497             HashRef containing options
1498              
1499             =over
1500              
1501             =item * return_old
1502              
1503             Bool whether return old, just deleted item or not
1504              
1505             Default: 0
1506              
1507             =item * no_cache
1508              
1509             Force not using cache, if enabled per default
1510              
1511             =item * use_cache
1512              
1513             Force using cache, if disabled per default but setupped
1514              
1515             =back
1516              
1517             =back
1518              
1519             =cut
1520              
1521             sub delete_item {
1522             my ( $self, $table, $where_ref, $args_ref ) = @_;
1523             $args_ref ||= {
1524             return_old => 0,
1525             no_cache => 0,
1526             use_cache => 0,
1527             max_retries => undef
1528             };
1529             $table = $self->_table_name( $table );
1530              
1531             # check definition
1532             my $table_ref = $self->_check_table( "delete_item", $table );
1533              
1534             # check primary keys
1535             croak "delete_item: Missing value for hash key '$table_ref->{ hash_key }'"
1536             unless defined $where_ref->{ $table_ref->{ hash_key } }
1537             && length( $where_ref->{ $table_ref->{ hash_key } } );
1538             croak "delete_item: Missing value for Range Key '$table_ref->{ range_key }'"
1539             if defined $table_ref->{ range_key } && ! (
1540             defined $where_ref->{ $table_ref->{ range_key } }
1541             && length( $where_ref->{ $table_ref->{ range_key } } )
1542             );
1543              
1544             # check other attributes
1545             $self->_check_keys( "delete_item: where-clause", $table, $where_ref );
1546              
1547             # build delete
1548             my %delete = (
1549             TableName => $table,
1550             Key => {},
1551             ( $args_ref->{ return_old } ? ( ReturnValues => 'ALL_OLD' ) : () )
1552             );
1553              
1554             # setup pk
1555             my %where = %$where_ref;
1556              
1557             # for hash key
1558             my $hash_value = delete $where{ $table_ref->{ hash_key } };
1559             $delete{ Key }->{ HashKeyElement } = {
1560             $self->_attrib_type( $table, $table_ref->{ hash_key } ) => $hash_value
1561             };
1562              
1563             # for range key
1564             if ( defined $table_ref->{ range_key } ) {
1565             my $range_value = delete $where{ $table_ref->{ range_key } };
1566             $delete{ Key }->{ RangeKeyElement } = {
1567             $self->_attrib_type( $table, $table_ref->{ range_key } ) => $range_value
1568             };
1569             }
1570              
1571             # build filter for other attribs
1572             if ( keys %where ) {
1573             $self->_build_attrib_filter( $table, \%where, $delete{ Expected } = {} );
1574             }
1575              
1576             # perform create
1577             my ( $res, $res_ok, $json_ref ) = $self->request( DeleteItem => \%delete, {
1578             max_retries => $args_ref->{ max_retries },
1579             } );
1580              
1581             if ( $res_ok ) {
1582              
1583             # use cache
1584             if ( $self->_cache_enabled( $args_ref ) ) {
1585             my $cache_key = $self->_cache_key_single( $table, $where_ref );
1586             $self->cache->remove( $cache_key );
1587             }
1588              
1589             if ( defined $json_ref->{ Attributes } ) {
1590             my %res;
1591             foreach my $attrib( $self->_attribs( $table ) ) {
1592             next unless defined $json_ref->{ Attributes }->{ $attrib };
1593             $res{ $attrib } = $json_ref->{ Attributes }->{ $attrib }->{ $self->_attrib_type( $table, $attrib ) };
1594             }
1595             return \%res;
1596             }
1597             return {};
1598             }
1599              
1600             $self->error( 'delete_item failed: '. $self->_extract_error_message( $res ) );
1601             return;
1602             }
1603              
1604              
1605              
1606             =head2 query_items $table, $where, $args
1607              
1608             Search in a table with hash AND range key.
1609              
1610             my ( $count, $items_ref, $next_start_keys_ref )
1611             = $ddb->qyery_items( some_table => { id => 123, my_range_id => { GT => 5 } } );
1612             print "Found $count items, where last id is ". $items_ref->[-1]->{ id }. "\n";
1613              
1614             # iterate through al all "pages"
1615             my $next_start_keys_ref;
1616             do {
1617             ( my $count, my $items_ref, $next_start_keys_ref )
1618             = $ddb->qyery_items( some_table => { id => 123, my_range_id => { GT => 5 } }, {
1619             start_key => $next_start_keys_ref
1620             } );
1621             } while( $next_start_keys_ref );
1622              
1623             =over
1624              
1625             =item * $table
1626              
1627             Name of the table
1628              
1629             =item * $where
1630              
1631             Search condition. Has to contain a value of the primary key and a search-value for the range key.
1632              
1633             Search-value for range key can be formated in two ways
1634              
1635             =over
1636              
1637             =item * Scalar
1638              
1639             Eg
1640              
1641             { $range_key_name => 123 }
1642              
1643             Performs and EQ (equal) search
1644              
1645             =item * HASHREF
1646              
1647             Eg
1648              
1649             { $range_key_name => { GT => 1 } }
1650             { $range_key_name => { CONTAINS => "Bla" } }
1651             { $range_key_name => { IN => [ 1, 2, 5, 7 ] } }
1652              
1653             See L<http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/API_Query.html>
1654              
1655             =back
1656              
1657             =item * $args
1658              
1659             {
1660             limit => 5,
1661             consistent => 0,
1662             backward => 0,
1663             #start_key => { .. }
1664             attributes => [ qw/ attrib1 attrib2 / ],
1665             #count => 1
1666             }
1667              
1668             HASHREF containing:
1669              
1670             =over
1671              
1672             =item * limit
1673              
1674             Amount of items to return
1675              
1676             Default: unlimited
1677              
1678             =item * consistent
1679              
1680             If set to 1, consistent read is performed
1681              
1682             Default: 0
1683              
1684             =item * backward
1685              
1686             Whether traverse index backward or forward.
1687              
1688             Default: 0 (=forward)
1689              
1690             =item * start_key
1691              
1692             Contains start key, as return in C<LastEvaluatedKey> from previous query. Allows to iterate above a table in pages.
1693              
1694             { $hash_key => 5, $range_key => "something" }
1695              
1696             =item * attributes
1697              
1698             Return only those attributes
1699              
1700             [ qw/ attrib attrib2 / ]
1701              
1702             =item * count
1703              
1704             Instead of returning the actual result, return the count.
1705              
1706             Default: 0 (=return result)
1707              
1708             =item * all
1709              
1710             Iterate through all pages (see link to API above) and return them all.
1711              
1712             Can take some time. Also: max_retries might be needed to set, as a scan/query create lot's of read-units, and an immediate reading of the next "pages" lead to an Exception due to too many reads.
1713              
1714             Default: 0 (=first "page" of items)
1715              
1716             =back
1717              
1718             =back
1719              
1720              
1721             =cut
1722              
1723             sub query_items {
1724             my ( $self, $table, $filter_ref, $args_ref ) = @_;
1725             my $table_orig = $table;
1726             $table = $self->_table_name( $table );
1727             $args_ref ||= {
1728             limit => undef, # amount of items
1729             consistent => 0, # default: eventually, not hard, conistent
1730             backward => 0, # default: forward
1731             start_key => undef, # eg { pk_name => 123, pk_other => 234 }
1732             attributes => undef, # eq [ qw/ attrib1 attrib2 / ]
1733             count => 0, # returns amount instead of the actual result
1734             all => 0, # read all entries (runs possibly multiple queries)
1735             max_retries => undef, # overwrite default max rewrites
1736             };
1737              
1738             # check definition
1739             croak "query_items: Table '$table' does not exist in table definition"
1740             unless defined $self->tables->{ $table };
1741             my $table_ref = $self->tables->{ $table };
1742              
1743             # die "query_items: Can run query_items only on tables with range key! '$table' does not have a range key.."
1744             # unless defined $table_ref->{ range_key };
1745              
1746             # build put
1747             my %query = (
1748             TableName => $table,
1749             ConsistentRead => $args_ref->{ consistent } ? \1 : \0,
1750             ScanIndexForward => $args_ref->{ backward } ? \0 : \1,
1751             ( defined $args_ref->{ limit } ? ( Limit => $args_ref->{ limit } ) : () ),
1752             );
1753              
1754             # using filter
1755             my %filter = %$filter_ref;
1756              
1757             if ( defined $filter{ $table_ref->{ hash_key } } ) {
1758             croak "query_items: Missing hash key value in filter-clause"
1759             unless defined $filter{ $table_ref->{ hash_key } };
1760             $query{ HashKeyValue } = {
1761             $self->_attrib_type( $table, $table_ref->{ hash_key } ) =>
1762             ( delete $filter{ $table_ref->{ hash_key } } ) . ''
1763             };
1764             }
1765              
1766             # adding range to filter
1767             if ( defined $table_ref->{ range_key }) {
1768             croak "query_items: Missing range key value in filter-clause"
1769             unless defined $filter{ $table_ref->{ range_key } };
1770             # r_ref = { GT => 1 } OR { BETWEEN => [ 1, 5 ] } OR { EQ => [ 1 ] } OR 5 FOR { EQ => 5 }
1771             my $r_ref = delete $filter{ $table_ref->{ range_key } };
1772             $r_ref = { EQ => $r_ref } unless ref( $r_ref );
1773             my ( $op, $vals_ref ) = %$r_ref;
1774             $vals_ref = [ $vals_ref ] unless ref( $vals_ref );
1775             my $type = $self->_attrib_type( $table, $table_ref->{ range_key } );
1776             $query{ RangeKeyCondition } = {
1777             AttributeValueList => [ map {
1778             { $type => $_. '' }
1779             } @$vals_ref ],
1780             ComparisonOperator => uc( $op )
1781             };
1782             }
1783              
1784             # too much keys
1785             croak "query_items: Cannot use keys ". join( ', ', sort keys %filter ). " in in filter - only hash and range key allowed."
1786             if keys %filter;
1787              
1788              
1789             # with start key?
1790             if( defined( my $start_key_ref = $args_ref->{ start_key } ) ) {
1791             $self->_check_keys( "query_items: start_key", $table, $start_key_ref );
1792             my $e_ref = $query{ ExclusiveStartKey } = {};
1793              
1794             # add hash key
1795             if ( defined $start_key_ref->{ $table_ref->{ hash_key } } ) {
1796             my $type = $self->_attrib_type( $table, $table_ref->{ hash_key } );
1797             $e_ref->{ HashKeyElement } = { $type => $start_key_ref->{ $table_ref->{ hash_key } } };
1798             }
1799              
1800             # add range key?
1801             if ( defined $table_ref->{ range_key } && defined $start_key_ref->{ $table_ref->{ range_key } } ) {
1802             my $type = $self->_attrib_type( $table, $table_ref->{ range_key } );
1803             $e_ref->{ RangeKeyElement } = { $type => $start_key_ref->{ $table_ref->{ range_key } } };
1804             }
1805             }
1806              
1807             # only certain attributes
1808             if ( defined( my $attribs_ref = $args_ref->{ attributes } ) ) {
1809             my @keys = $self->_check_keys( "query_items: attributes", $table, $attribs_ref );
1810             $query{ AttributesToGet } = \@keys;
1811             }
1812              
1813             # or count?
1814             elsif ( $args_ref->{ count } ) {
1815             $query{ Count } = \1;
1816             }
1817              
1818             # perform query
1819             #print Dumper( { QUERY => \%query } );
1820             my ( $res, $res_ok, $json_ref ) = $self->request( Query => \%query, {
1821             max_retries => $args_ref->{ max_retries },
1822             } );
1823              
1824             # format & return result
1825             if ( $res_ok && defined $json_ref->{ Items } ) {
1826             my @res;
1827             foreach my $from_ref( @{ $json_ref->{ Items } } ) {
1828             push @res, $self->_format_item( $table, $from_ref );
1829             }
1830             my $count = $json_ref->{ Count };
1831              
1832             # build start key for return or use
1833             my $next_start_key_ref;
1834             if ( defined $json_ref->{ LastEvaluatedKey } ) {
1835             $next_start_key_ref = {};
1836              
1837             # add hash key to start key
1838             my $hash_type = $self->_attrib_type( $table, $table_ref->{ hash_key } );
1839             $next_start_key_ref->{ $table_ref->{ hash_key } } = $json_ref->{ LastEvaluatedKey }->{ HashKeyElement }->{ $hash_type };
1840              
1841             # add range key to start key
1842             if ( defined $table_ref->{ range_key } && defined $json_ref->{ LastEvaluatedKey }->{ RangeKeyElement } ) {
1843             my $range_type = $self->_attrib_type( $table, $table_ref->{ range_key } );
1844             $next_start_key_ref->{ $table_ref->{ range_key } } = $json_ref->{ LastEvaluatedKey }->{ RangeKeyElement }->{ $range_type };
1845             }
1846             }
1847              
1848             # cycle through all?
1849             if ( $args_ref->{ all } && $next_start_key_ref ) {
1850              
1851             # make sure we do not run into a loop by comparing last and current start key
1852             my $new_start_key = join( ';', map { sprintf( '%s=%s', $_, $next_start_key_ref->{ $_ } ) } sort keys %$next_start_key_ref );
1853             my %key_cache = defined $args_ref->{ _start_key_cache } ? %{ $args_ref->{ _start_key_cache } } : ();
1854             #print Dumper( { STARTKEY => $next_start_key_ref, LASTEVAL => $json_ref->{ LastEvaluatedKey }, KEYS => [ \%key_cache, $new_start_key ] } );
1855              
1856             if ( ! defined $key_cache{ $new_start_key } ) {
1857             $key_cache{ $new_start_key } = 1;
1858              
1859             # perform sub-query
1860             my ( $sub_count, $sub_res_ref ) = $self->query_items( $table_orig, $filter_ref, {
1861             %$args_ref,
1862             _start_key_cache => \%key_cache,
1863             start_key => $next_start_key_ref
1864             } );
1865             #print Dumper( { SUB_COUNT => $sub_count } );
1866              
1867             # add result
1868             if ( $sub_count ) {
1869             $count += $sub_count;
1870             push @res, @$sub_res_ref;
1871             }
1872             }
1873             }
1874              
1875             return wantarray ? ( $count, \@res, $next_start_key_ref ) : \@res;
1876             }
1877              
1878             # error
1879             $self->error( 'query_items failed: '. $self->_extract_error_message( $res ) );
1880             return;
1881             }
1882              
1883              
1884              
1885             =head2 scan_items $table, $filter, $args
1886              
1887             Performs scan on table. The result is B<eventually consistent>. Non hash or range keys are allowed in the filter.
1888              
1889             See query_items for argument description.
1890              
1891             Main difference to query_items: A whole table scan is performed, which is much slower. Also the amount of data scanned is limited in size; see L<http://docs.amazonwebservices.com/amazondynamodb/latest/developerguide/API_Scan.html>
1892              
1893             =cut
1894              
1895             sub scan_items {
1896             my ( $self, $table, $filter_ref, $args_ref ) = @_;
1897             my $table_orig = $table;
1898             $table = $self->_table_name( $table );
1899             $args_ref ||= {
1900             limit => undef, # amount of items
1901             start_key => undef, # eg { hash_key => 1, range_key => "bla" }
1902             attributes => undef, # eq [ qw/ attrib1 attrib2 / ]
1903             count => 0, # returns amount instead of the actual result
1904             all => 0, # read all entries (runs possibly multiple queries)
1905             max_retries => undef, # overwrite default max retries
1906             };
1907              
1908             # check definition
1909             croak "scan_items: Table '$table' does not exist in table definition"
1910             unless defined $self->tables->{ $table };
1911             my $table_ref = $self->tables->{ $table };
1912              
1913             # build put
1914             my %query = (
1915             TableName => $table,
1916             ScanFilter => {},
1917             ( defined $args_ref->{ limit } ? ( Limit => $args_ref->{ limit } ) : () ),
1918             );
1919              
1920             # using filter
1921             if ( $filter_ref && keys %$filter_ref ) {
1922             my @filter_keys = $self->_check_keys( "scan_items: filter keys", $table, $filter_ref );
1923             my $s_ref = $query{ ScanFilter };
1924             foreach my $key( @filter_keys ) {
1925             my $type = $self->_attrib_type( $table, $key );
1926             my $val_ref = $filter_ref->{ $key };
1927             my $rvalue = ref( $val_ref ) || '';
1928             if ( $rvalue eq 'HASH' ) {
1929             my ( $op, $value ) = %$val_ref;
1930             $s_ref->{ $key } = {
1931             AttributeValueList => [ { $type => $value. '' } ],
1932             ComparisonOperator => uc( $op )
1933             };
1934             }
1935             elsif( $rvalue eq 'ARRAY' ) {
1936             $s_ref->{ $key } = {
1937             AttributeValueList => [ { $type => $val_ref } ],
1938             ComparisonOperator => 'IN'
1939             };
1940             }
1941             else {
1942             $s_ref->{ $key } = {
1943             AttributeValueList => [ { $type => $val_ref. '' } ],
1944             ComparisonOperator => 'EQ'
1945             };
1946             }
1947             }
1948             }
1949              
1950             # with start key?
1951             if( defined( my $start_key_ref = $args_ref->{ start_key } ) ) {
1952             $self->_check_keys( "scan_items: start_key", $table, $start_key_ref );
1953             my $e_ref = $query{ ExclusiveStartKey } = {};
1954              
1955             # add hash key
1956             if ( defined $start_key_ref->{ $table_ref->{ hash_key } } ) {
1957             my $type = $self->_attrib_type( $table, $table_ref->{ hash_key } );
1958             $e_ref->{ HashKeyElement } = { $type => $start_key_ref->{ $table_ref->{ hash_key } } };
1959             }
1960              
1961             # add range key?
1962             if ( defined $table_ref->{ range_key } && defined $start_key_ref->{ $table_ref->{ range_key } } ) {
1963             my $type = $self->_attrib_type( $table, $table_ref->{ range_key } );
1964             $e_ref->{ RangeKeyElement } = { $type => $start_key_ref->{ $table_ref->{ range_key } } };
1965             }
1966             }
1967              
1968             # only certain attributes
1969             if ( defined( my $attribs_ref = $args_ref->{ attributes } ) ) {
1970             my @keys = $self->_check_keys( "scan_items: attributes", $table, $attribs_ref );
1971             $query{ AttributesToGet } = \@keys;
1972             }
1973              
1974             # or count?
1975             elsif ( $args_ref->{ count } ) {
1976             $query{ Count } = \1;
1977             }
1978              
1979             # perform query
1980             my ( $res, $res_ok, $json_ref ) = $self->request( Scan => \%query, {
1981             max_retries => $args_ref->{ max_retries },
1982             } );
1983              
1984             # format & return result
1985             if ( $res_ok && defined $json_ref->{ Items } ) {
1986             my @res;
1987             foreach my $from_ref( @{ $json_ref->{ Items } } ) {
1988             push @res, $self->_format_item( $table, $from_ref );
1989             }
1990              
1991             my $count = $json_ref->{ Count };
1992              
1993             # build start key for return or use
1994             my $next_start_key_ref;
1995             if ( defined $json_ref->{ LastEvaluatedKey } ) {
1996             $next_start_key_ref = {};
1997              
1998             # add hash key to start key
1999             my $hash_type = $self->_attrib_type( $table, $table_ref->{ hash_key } );
2000             $next_start_key_ref->{ $table_ref->{ hash_key } } = $json_ref->{ LastEvaluatedKey }->{ HashKeyElement }->{ $hash_type };
2001              
2002             # add range key to start key
2003             if ( defined $table_ref->{ range_key } && defined $json_ref->{ LastEvaluatedKey }->{ RangeKeyElement } ) {
2004             my $range_type = $self->_attrib_type( $table, $table_ref->{ range_key } );
2005             $next_start_key_ref->{ $table_ref->{ range_key } } = $json_ref->{ LastEvaluatedKey }->{ RangeKeyElement }->{ $range_type };
2006             }
2007             }
2008              
2009             # cycle through all?
2010             if ( $args_ref->{ all } && $next_start_key_ref ) {
2011              
2012             # make sure we do not run into a loop by comparing last and current start key
2013             my $new_start_key = join( ';', map { sprintf( '%s=%s', $_, $next_start_key_ref->{ $_ } ) } sort keys %$next_start_key_ref );
2014             my %key_cache = defined $args_ref->{ _start_key_cache } ? %{ $args_ref->{ _start_key_cache } } : ();
2015             #print Dumper( { STARTKEY => $next_start_key_ref, LASTEVAL => $json_ref->{ LastEvaluatedKey }, KEYS => [ \%key_cache, $new_start_key ] } );
2016              
2017             if ( ! defined $key_cache{ $new_start_key } ) {
2018             $key_cache{ $new_start_key } = 1;
2019              
2020             # perform sub-query
2021             my ( $sub_count, $sub_res_ref ) = $self->scan_items( $table_orig, $filter_ref, {
2022             %$args_ref,
2023             _start_key_cache => \%key_cache,
2024             start_key => $next_start_key_ref
2025             } );
2026             #print Dumper( { SUB_COUNT => $sub_count } );
2027              
2028             # add result
2029             if ( $sub_count ) {
2030             $count += $sub_count;
2031             push @res, @$sub_res_ref;
2032             }
2033             }
2034             }
2035              
2036             return wantarray ? ( $count, \@res, $next_start_key_ref ) : \@res;
2037             }
2038              
2039             # error
2040             $self->error( 'scan_items failed: '. $self->_extract_error_message( $res ) );
2041             return;
2042             }
2043              
2044              
2045              
2046             =head2 request
2047              
2048             Arbitrary request to DynamoDB API
2049              
2050             =cut
2051              
2052             sub request {
2053             my ( $self, $target, $json, $args_ref ) = @_;
2054             $args_ref ||= {
2055             max_retries => undef
2056             };
2057              
2058             # assure security token existing
2059             unless( $self->_init_security_token() ) {
2060             my %error = ( error => $self->error() );
2061             return wantarray ? ( undef, 0, \%error ) : \%error;
2062             }
2063              
2064             # convert to string, if required
2065             $json = $self->json->encode( $json ) if ref $json;
2066              
2067             # get date
2068             my $http_date = DateTime::Format::HTTP->format_datetime( DateTime->now );
2069              
2070             # build signable content
2071             #$json is already utf8 encoded via json encode
2072             my $sign_content = encode_utf8(join( "\n",
2073             'POST', '/', '',
2074             'host:'. $self->host,
2075             'x-amz-date:'. $http_date,
2076             'x-amz-security-token:'. $self->_credentials->{ SessionToken },
2077             'x-amz-target:DynamoDB_20111205.'. $target,
2078             ''
2079             )) . "\n" . $json ;
2080             my $signature = hmac_sha256_base64( sha256( $sign_content ), $self->_credentials->{ SecretAccessKey } );
2081             $signature .= '=' while( length( $signature ) % 4 != 0 );
2082              
2083             # build request
2084             my $request = HTTP::Request->new( POST => 'http://'. $self->host. '/' );
2085              
2086             # .. setup headers
2087             $request->header( host => $self->host );
2088             $request->header( 'x-amz-date' => $http_date );
2089             $request->header( 'x-amz-target', 'DynamoDB_'. $self->api_version. '.'. $target );
2090             $request->header( 'x-amzn-authorization' => join( ',',
2091             'AWS3 AWSAccessKeyId='. $self->_credentials->{ AccessKeyId },
2092             'Algorithm=HmacSHA256',
2093             'SignedHeaders=host;x-amz-date;x-amz-security-token;x-amz-target',
2094             'Signature='. $signature
2095             ) );
2096             $request->header( 'x-amz-security-token' => $self->_credentials->{ SessionToken } );
2097             $request->header( 'content-type' => 'application/x-amz-json-1.0' );
2098              
2099             # .. add content
2100             $request->content( $json );
2101              
2102             my ( $json_ref, $response );
2103             my $tries = defined $args_ref->{ max_retries }
2104             ? $args_ref->{ max_retries }
2105             : $self->max_retries + 1;
2106             while( 1 ) {
2107              
2108             # run request
2109             $response = $self->lwp->request( $request );
2110             $ENV{ DYNAMO_DB_DEBUG } && warn Dumper( $response );
2111             $ENV{ DYNAMO_DB_DEBUG_KEEPALIVE } && warn " LWP keepalives in use: ", scalar($self->_lwpcache()->get_connections()), "/", $self->_lwpcache()->total_capacity(), "\n";
2112              
2113             # get json
2114             $json_ref = $response
2115             ? eval { $self->json->decode( $response->decoded_content ) } || { error => "Failed to parse JSON result" }
2116             : { error => "Failed to get result" };
2117             if ( defined $json_ref->{ __type } && $json_ref->{ __type } =~ /ProvisionedThroughputExceededException/ && $tries-- > 0 ) {
2118             $ENV{ DYNAMO_DB_DEBUG_RETRY } && warn "Retry $target: $json\n";
2119             usleep( $self->retry_timeout * 1_000_000 );
2120             next;
2121             }
2122             last;
2123             }
2124              
2125              
2126             # handle error
2127             if ( defined $json_ref->{ error } && $json_ref->{ error } ) {
2128             $self->error( $json_ref->{ error } );
2129             }
2130              
2131             # handle exception
2132             elsif ( defined $json_ref->{ __type } && $json_ref->{ __type } =~ /Exception/ && $json_ref->{ Message } ) {
2133             $self->error( $json_ref->{ Message } );
2134             }
2135              
2136             return wantarray ? ( $response, $response ? $response->is_success : 0, $json_ref ) : $json_ref;
2137             }
2138              
2139              
2140              
2141             =head2 error [$str]
2142              
2143             Get/set last error
2144              
2145             =cut
2146              
2147             sub error {
2148             my ( $self, $str ) = @_;
2149             if ( $str ) {
2150             croak $str if $self->raise_error();
2151             $self->_error( $str );
2152             }
2153             return $self->_error if $self->_has_error;
2154             return ;
2155             }
2156              
2157              
2158              
2159             #
2160             # _init_security_token
2161             # Creates new temporary security token (, access and secret key), if not exist
2162             #
2163              
2164             sub _init_security_token {
2165             my ( $self ) = @_;
2166              
2167             # wheter has valid credentials
2168             if ( $self->_has_credentials() ) {
2169             my $dt = DateTime->now( time_zone => 'local' )->add( seconds => 5 );
2170             return 1 if $dt < $self->_credentials_expire;
2171             }
2172              
2173             # build aws signed request
2174             $self->_aws_signer( Net::Amazon::AWSSign->new(
2175             $self->access_key, $self->secret_key ) )
2176             unless $self->_has_aws_signer;
2177             my $url = $self->_aws_signer->addRESTSecret( $self->_security_token_url );
2178              
2179             # get token
2180             my $res = $self->lwp->get( $url );
2181              
2182             # got response
2183             if ( $res->is_success) {
2184             my $content = $res->decoded_content;
2185             my $result_ref = XMLin( $content );
2186              
2187             # got valid result
2188             if( ref $result_ref && defined $result_ref->{ GetSessionTokenResult }
2189             && defined $result_ref->{ GetSessionTokenResult }
2190             && defined $result_ref->{ GetSessionTokenResult }->{ Credentials }
2191             ) {
2192             # SessionToken, AccessKeyId, Expiration, SecretAccessKey
2193             my $cred_ref = $result_ref->{ GetSessionTokenResult }->{ Credentials };
2194             if ( ref( $cred_ref )
2195             && defined $cred_ref->{ SessionToken }
2196             && defined $cred_ref->{ AccessKeyId }
2197             && defined $cred_ref->{ SecretAccessKey }
2198             && defined $cred_ref->{ Expiration }
2199             ) {
2200             # parse expiration date
2201             my $pattern = DateTime::Format::Strptime->new(
2202             pattern => '%FT%T',
2203             time_zone => 'UTC'
2204             );
2205             my $expire = $pattern->parse_datetime( $cred_ref->{ Expiration } );
2206             $expire->set_time_zone( 'local' );
2207             $self->_credentials_expire( $expire );
2208              
2209             # set credentials
2210             $self->_credentials( $cred_ref );
2211             return 1;
2212             }
2213             }
2214             else {
2215             $self->error( "Failed to fetch credentials: ". $res->status_line. " ($content)" );
2216             }
2217             }
2218             else {
2219             my $content = eval { $res->decoded_content } || "No Content";
2220             $self->error( "Failed to fetch credentials: ". $res->status_line. " ($content)" );
2221             }
2222              
2223             return 0;
2224             }
2225              
2226              
2227             #
2228             # _check_table $table
2229             # Check whether table exists and returns definition
2230             #
2231              
2232             sub _check_table {
2233             my ( $self, $meth, $table ) = @_;
2234             unless( $table ) {
2235             $table = $meth;
2236             $meth = "check_table";
2237             }
2238             croak "$meth: Table '$table' not defined"
2239             unless defined $self->tables->{ $table };
2240              
2241             return $self->tables->{ $table };
2242             }
2243              
2244              
2245             #
2246             # _check_keys $meth, $table, $key_ref
2247             # Check attributes. Dies on invalid (not registererd) attributes.
2248             #
2249              
2250             sub _check_keys {
2251             my ( $self, $meth, $table, $key_ref ) = @_;
2252             my $table_ref = $self->_check_table( $meth, $table );
2253              
2254             my @keys = ref( $key_ref )
2255             ? ( ref( $key_ref ) eq 'ARRAY'
2256             ? @$key_ref
2257             : keys %$key_ref
2258             )
2259             : ( $key_ref )
2260             ;
2261              
2262             my @invalid_keys = grep { ! defined $table_ref->{ attributes }->{ $_ } } @keys;
2263             croak "$meth: Invalid keys: ". join( ', ', @invalid_keys )
2264             if @invalid_keys;
2265              
2266             return wantarray ? @keys : \@keys;
2267             }
2268              
2269              
2270             #
2271             # _build_pk_filter $table, $where_ref, $node_ref
2272             # Build attribute filter "HashKeyElement" and "RangeKeyElement".
2273             # Hash key and range key will be deleted from where clause
2274             #
2275              
2276             sub _build_pk_filter {
2277             my ( $self, $table, $where_ref, $node_ref ) = @_;
2278             # primary key
2279             my $table_ref = $self->_check_table( $table );
2280             my $hash_value = delete $where_ref->{ $table_ref->{ hash_key } };
2281             my $hash_type = $self->_attrib_type( $table, $table_ref->{ hash_key } );
2282             $node_ref->{ HashKeyElement } = { $hash_type => $hash_value . '' };
2283             if ( defined $table_ref->{ range_key } ) {
2284             my $range_value = delete $where_ref->{ $table_ref->{ range_key } };
2285             my $range_type = $self->_attrib_type( $table, $table_ref->{ range_key } );
2286             $node_ref->{ RangeKeyElement } = { $range_type => $range_value . '' };
2287             }
2288             }
2289              
2290              
2291             #
2292             # _build_attrib_filter $table, $where_ref, $node_ref
2293             # Build attribute filter "Expected" from given where-clause-ref
2294             # {
2295             # attrib1 => 'somevalue', # -> { attrib1 => { Value => { S => 'somevalue' } } }
2296             # attrib2 => \1, # -> { attrib2 => { Exists => true } }
2297             # attrib3 => { # -> { attrib3 => { Value => { S => 'bla' } } }
2298             # value => 'bla'
2299             # }
2300             # }
2301             #
2302              
2303             sub _build_attrib_filter {
2304             my ( $self, $table, $where_ref, $node_ref ) = @_;
2305             my $table_ref = $self->_check_table( $table );
2306             foreach my $key( keys %$where_ref ){
2307             my $type = $table_ref->{ attributes }->{ $key };
2308             my %cur;
2309             unless( ref( $where_ref->{ $key } ) ) {
2310             $where_ref->{ $key } = { value => $where_ref->{ $key } };
2311             }
2312             if ( ref( $where_ref->{ $key } ) eq 'SCALAR' ) {
2313             $cur{ Exists } = $where_ref->{ $key };
2314             }
2315             else {
2316             if ( defined( my $value = $where_ref->{ $key }->{ value } ) ) {
2317             $cur{ Value } = { $type => $value. '' };
2318             }
2319             if ( defined $where_ref->{ $key }->{ exists } ) {
2320             $cur{ Exists } = $where_ref->{ $key }->{ exists } ? \1 : \0;
2321             }
2322             }
2323             $node_ref->{ $key } = \%cur if keys %cur;
2324             }
2325             }
2326              
2327              
2328             #
2329             # _attrib_type $table, $key
2330             # Returns type ("S", "N", "NS", "SS") of existing attribute in table
2331             #
2332              
2333             sub _attrib_type {
2334             my ( $self, $table, $key ) = @_;
2335             my $table_ref = $self->_check_table( $table );
2336             return defined $table_ref->{ attributes }->{ $key } ? $table_ref->{ attributes }->{ $key } : "S";
2337             }
2338              
2339              
2340             #
2341             # _attribs $table
2342             # Returns list of attributes in table
2343             #
2344              
2345             sub _attribs {
2346             my ( $self, $table ) = @_;
2347             my $table_ref = $self->_check_table( $table );
2348             return sort keys %{ $table_ref->{ attributes } };
2349             }
2350              
2351              
2352             #
2353             # _format_item $table, $from_ref
2354             #
2355             # Formats result item into simpler format
2356             # {
2357             # attrib => { S => "bla" }
2358             # }
2359             #
2360             # to
2361             # {
2362             # attrib => 'bla'
2363             # }
2364             #
2365              
2366             sub _format_item {
2367             my ( $self, $table, $from_ref ) = @_;
2368             my $table_ref = $self->_check_table( format_item => $table );
2369             my %formatted;
2370             if ( defined $from_ref->{ HashKeyElement } ) {
2371             my @keys = ( 'hash' );
2372             push @keys, 'range' if defined $table_ref->{ range_key };
2373             foreach my $key( @keys ) {
2374             my $key_name = $table_ref->{ "${key}_key" };
2375             my $key_type = $table_ref->{ attributes }->{ $key_name };
2376             $formatted{ $key_name } = $from_ref->{ ucfirst( $key ). 'KeyElement' }->{ $key_type };
2377             }
2378             }
2379             else {
2380             if ( $self->derive_table() ) {
2381             while ( my ( $key, $value ) = each %$from_ref ) {
2382             $formatted{$key} = ( $value->{'S'} || $value->{'N'} || $value->{'NS'} || $value->{'SS'} );
2383             }
2384             }
2385             else {
2386             while( my( $attrib, $type ) = each %{ $table_ref->{ attributes } } ) {
2387             next unless defined $from_ref->{ $attrib };
2388             $formatted{ $attrib } = $from_ref->{ $attrib }->{ $type };
2389             }
2390             }
2391             }
2392             return \%formatted;
2393             }
2394              
2395              
2396             #
2397             # _table_name
2398             # Returns prefixed table name
2399             #
2400              
2401             sub _table_name {
2402             my ( $self, $table, $remove ) = @_;
2403             return $remove ? substr( $table, length( $self->namespace ) ) : $self->namespace. $table;
2404             }
2405              
2406              
2407             #
2408             # _extract_error_message
2409             #
2410              
2411             sub _extract_error_message {
2412             my ( $self, $response ) = @_;
2413             my $msg = '';
2414             if ( $response ) {
2415             my $json = eval { $self->json->decode( $response->decoded_content ) } || { error => "Failed to parse JSON result" };
2416             if ( defined $json->{ __type } ) {
2417             $msg = join( ' ** ',
2418             "ErrorType: $json->{ __type }",
2419             "ErrorMessage: $json->{ message }",
2420             );
2421             }
2422             else {
2423             $msg = $json->{ error };
2424             }
2425             }
2426             else {
2427             $msg = 'No response received. DynamoDB down?'
2428             }
2429             }
2430              
2431             #
2432             # _cache_enabled
2433             #
2434              
2435             sub _cache_enabled {
2436             my ( $self, $args_ref ) = @_;
2437             return $self->has_cache && ! $args_ref->{ no_cache }
2438             && ( $args_ref->{ use_cache } || ! $self->cache_disabled );
2439             }
2440              
2441             #
2442             # _cache_key_single
2443             #
2444              
2445             sub _cache_key_single {
2446             my ( $self, $table, $hash_ref ) = @_;
2447             my $table_ref = $self->_check_table( $table );
2448             my @keys = ( $table_ref->{ hash_key } );
2449             push @keys, $table_ref->{ range_key } if defined $table_ref->{ range_key };
2450             my %pk = map { ( $_ => $hash_ref->{ $_ } || '' ) } @keys;
2451             return $self->_cache_key( $table, 'single', \%pk );
2452             }
2453              
2454             #
2455             # _cache_key
2456             #
2457              
2458             sub _cache_key {
2459             my ( $self, $table, $name, $id_ref ) = @_;
2460             my $method = $self->cache_key_method();
2461             return sprintf( '%s-%s-%s', $table, $name, $method->( $self->json->encode( $id_ref ) ) );
2462             }
2463              
2464             __PACKAGE__->meta->make_immutable;
2465              
2466              
2467             =head1 AUTHOR
2468              
2469             =over
2470              
2471             =item * Ulrich Kautz <uk@fortrabbit.de>
2472              
2473             =item * Thanks to MadHacker L<http://stackoverflow.com/users/1139526/madhacker> (the signing code in request method)
2474              
2475             =item * Benjamin Abbott-Scoot <benjamin@abbott-scott.net> (Keep Alive patch)
2476              
2477             =back
2478              
2479             =head1 COPYRIGHT
2480              
2481             Copyright (c) 2012 the L</AUTHOR> as listed above
2482              
2483             =head1 LICENCSE
2484              
2485             Same license as Perl itself.
2486              
2487             =cut
2488              
2489             1;