File Coverage

blib/lib/App/Dochazka/REST/Model/Shared.pm
Criterion Covered Total %
statement 32 296 10.8
branch 0 104 0.0
condition 0 29 0.0
subroutine 11 53 20.7
pod 20 20 100.0
total 63 502 12.5


line stmt bran cond sub pod time code
1             # *************************************************************************
2             # Copyright (c) 2014-2017, SUSE LLC
3             #
4             # All rights reserved.
5             #
6             # Redistribution and use in source and binary forms, with or without
7             # modification, are permitted provided that the following conditions are met:
8             #
9             # 1. Redistributions of source code must retain the above copyright notice,
10             # this list of conditions and the following disclaimer.
11             #
12             # 2. Redistributions in binary form must reproduce the above copyright
13             # notice, this list of conditions and the following disclaimer in the
14             # documentation and/or other materials provided with the distribution.
15             #
16             # 3. Neither the name of SUSE LLC nor the names of its contributors may be
17             # used to endorse or promote products derived from this software without
18             # specific prior written permission.
19             #
20             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
23             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
24             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
25             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
26             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
27             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
28             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
29             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
30             # POSSIBILITY OF SUCH DAMAGE.
31             # *************************************************************************
32              
33             package App::Dochazka::REST::Model::Shared;
34              
35 41     41   3301 use 5.012;
  41         175  
36 41     41   233 use strict;
  41         86  
  41         961  
37 41     41   218 use warnings;
  41         105  
  41         1261  
38              
39 41     41   284 use App::CELL qw( $CELL $log $meta $site );
  41         120  
  41         4171  
40 41     41   304 use Data::Dumper;
  41         85  
  41         2102  
41 41     41   14284 use JSON;
  41         220293  
  41         329  
42 41     41   5145 use Params::Validate qw( :all );
  41         490  
  41         6262  
43 41     41   333 use Try::Tiny;
  41         88  
  41         2470  
44              
45              
46              
47              
48             =head1 NAME
49              
50             App::Dochazka::REST::Model::Shared - functions shared by several modules within
51             the data model
52              
53              
54              
55              
56             =head1 SYNOPSIS
57              
58             use App::Dochazka::REST::Model::Shared;
59              
60             ...
61              
62              
63              
64              
65             =head1 EXPORTS
66              
67             =cut
68              
69 41     41   272 use Exporter qw( import );
  41         76  
  41         99382  
70             our @EXPORT_OK = qw(
71             canonicalize_date
72             canonicalize_ts
73             canonicalize_tsrange
74             cud
75             cud_generic
76             decode_schedule_json
77             get_history
78             load
79             load_multiple
80             noof
81             priv_by_eid
82             schedule_by_eid
83             select_single
84             select_set_of_single_scalar_rows
85             split_tsrange
86             timestamp_delta_minus
87             timestamp_delta_plus
88             tsrange_intersection
89             tsrange_equal
90             );
91              
92              
93              
94              
95             =head1 FUNCTIONS
96              
97              
98             =head2 canonicalize_date
99              
100             Given a string that PostgreSQL might recognize as a date, pass it to
101             the database via the SQL statement:
102              
103             SELECT CAST( ? AS date )
104              
105             and return the resulting status object.
106              
107             =cut
108              
109             sub canonicalize_date {
110 0     0 1 0 my ( $conn, $ts ) = @_;
111              
112 0         0 my $status = select_single(
113             conn => $conn,
114             sql => 'SELECT CAST( ? AS date )',
115             keys => [ $ts ],
116             );
117 0 0       0 _replace_payload_array_with_string( $status ) if $status->ok;
118 0         0 return $status;
119             }
120              
121              
122             =head2 canonicalize_ts
123              
124             Given a string that might be a timestamp, "canonicalize" it by running it
125             through the database in the SQL statement:
126              
127             SELECT CAST( ? AS timestamptz )
128              
129             =cut
130              
131             sub canonicalize_ts {
132 0     0 1 0 my ( $conn, $ts ) = @_;
133              
134 0         0 my $status = select_single(
135             conn => $conn,
136             sql => 'SELECT CAST( ? AS timestamptz )',
137             keys => [ $ts ],
138             );
139 0 0       0 _replace_payload_array_with_string( $status ) if $status->ok;
140 0         0 return $status;
141             }
142              
143             sub _replace_payload_array_with_string {
144 0     0   0 my $status = shift;
145 0         0 $status->payload( $status->payload->[0] );
146 0         0 return $status;
147             }
148              
149              
150             =head2 canonicalize_tsrange
151              
152             Given a string that might be a tsrange, "canonicalize" it by running it
153             through the database in the SQL statement:
154              
155             SELECT CAST( ? AS tstzrange )
156              
157             Returns an L<App::CELL::Status> object. If the status code is OK, then the
158             tsrange is OK and its canonicalized form is in the payload. Otherwise, some
159             kind of error occurred, as described in the status object.
160              
161             =cut
162              
163             sub canonicalize_tsrange {
164 0     0 1 0 my ( $conn, $tsr ) = @_;
165              
166 0         0 my $status = select_single(
167             conn => $conn,
168             sql => 'SELECT CAST( ? AS tstzrange)',
169             keys => [ $tsr ],
170             );
171 0 0       0 _replace_payload_array_with_string( $status ) if $status->ok;
172 0 0 0     0 return $CELL->status_err( 'DOCHAZKA_TSRANGE_EMPTY' ) if $status->ok and $status->payload eq "empty";
173 0         0 return $status;
174             }
175              
176              
177             =head2 cud
178              
179             Attempts to Create, Update, or Delete a single database record. Takes the
180             following PARAMHASH:
181              
182             =over
183              
184             =item * conn
185              
186             The L<DBIx::Connector> object with which to gain access to the database.
187              
188             =item * eid
189              
190             The EID of the employee originating the request (needed for the audit triggers).
191              
192             =item * object
193              
194             The Dochazka datamodel object to be worked on.
195              
196             =item * sql
197              
198             The SQL statement to execute (should be INSERT, UPDATE, or DELETE).
199              
200             =item * attrs
201              
202             An array reference containing the bind values to be plugged into the SQL
203             statement.
204              
205             =back
206              
207             Returns a status object.
208              
209             Important note: it is up to the programmer to not pass any SQL statement that
210             might affect more than one record.
211              
212             =cut
213              
214             sub cud {
215 0     0 1 0 my %ARGS = validate( @_, {
216             conn => { isa => 'DBIx::Connector' },
217             eid => { type => SCALAR },
218             object => { can => [ qw( insert delete ) ] },
219             sql => { type => SCALAR },
220             attrs => { type => ARRAYREF }, # order of attrs must match SQL statement
221             } );
222              
223 0         0 my ( $status, $rv, $count );
224              
225             try {
226             local $SIG{__WARN__} = sub {
227 0         0 die @_;
228 0     0   0 };
229              
230             # start transaction
231             $ARGS{'conn'}->txn( fixup => sub {
232              
233             # get DBI db handle
234 0         0 my $dbh = shift;
235              
236             # set the dochazka.eid GUC session parameter
237 0         0 $dbh->do( $site->SQL_SET_DOCHAZKA_EID_GUC, undef, ( $ARGS{'eid'}+0 ) );
238              
239             # prepare the SQL statement and bind parameters
240 0         0 my $sth = $dbh->prepare( $ARGS{'sql'} );
241 0         0 my $counter = 0;
242             map {
243 0         0 $counter += 1;
244 0         0 $sth->bind_param( $counter, $ARGS{'object'}->{$_} );
245 0         0 } @{ $ARGS{'attrs'} };
  0         0  
246              
247             # execute the SQL statement
248 0         0 $rv = $sth->execute;
249 0         0 $log->debug( "cud: DBI execute returned " . Dumper( $rv ) );
250 0 0       0 if ( $rv == 1 ) {
    0          
    0          
    0          
251              
252             # a record was returned; get the values
253 0         0 my $rh = $sth->fetchrow_hashref;
254 0         0 $log->info( "Statement " . $sth->{'Statement'} . " RETURNING values: " . Dumper( $rh ) );
255             # populate object with all RETURNING fields
256 0         0 map { $ARGS{'object'}->{$_} = $rh->{$_}; } ( keys %$rh );
  0         0  
257              
258             # count number of rows affected
259 0         0 $count = $sth->rows;
260              
261             } elsif ( $rv eq '0E0' ) {
262              
263             # no error, but no record returned either
264 0         0 $count = $sth->rows;
265              
266             } elsif ( $rv > 1 ) {
267             $status = $CELL->status_crit(
268             'DOCHAZKA_CUD_MORE_THAN_ONE_RECORD_AFFECTED',
269 0         0 args => [ $sth->{'Statement'} ]
270             );
271             } elsif ( $rv == -1 ) {
272             $status = $CELL->status_err(
273             'DOCHAZKA_CUD_UNKNOWN_NUMBER_OF_RECORDS_AFFECTED',
274 0         0 args => [ $sth->{'Statement'} ]
275             );
276             } else {
277 0         0 $status = $CELL->status_crit( 'DOCHAZKA_DBI_EXECUTE_WEIRDNESS' );
278             }
279 0         0 } );
280             } catch {
281 0     0   0 my $errmsg = $_;
282 0 0       0 if ( not defined( $errmsg ) ) {
283 0         0 $log->err( '$_ undefined in catch' );
284 0         0 $errmsg = '<NONE>';
285             }
286 0 0       0 if ( ! $site->DOCHAZKA_SQL_TRACE ) {
287 0         0 $errmsg =~ s/^DBD::Pg::st execute failed: //;
288 0         0 $errmsg =~ s#at /usr/lib/perl5/.* line .*\.$##;
289             }
290 0 0       0 if ( ! defined( $status ) ) {
291 0         0 $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR',
292             args => [ $errmsg ],
293             DBI_return_value => $rv,
294             );
295             }
296 0         0 };
297              
298 0 0       0 if ( not defined( $status ) ) {
299             $status = $CELL->status_ok( 'DOCHAZKA_CUD_OK',
300             DBI_return_value => $rv,
301 0         0 payload => $ARGS{'object'},
302             count => $count,
303             );
304             }
305              
306 0         0 return $status;
307             }
308              
309              
310             =head2 cud_generic
311              
312             Attempts to execute a generic Create, Update, or Delete database operation.
313             Takes the following PARAMHASH:
314              
315             =over
316              
317             =item * conn
318              
319             The L<DBIx::Connector> object with which to gain access to the database.
320              
321             =item * eid
322              
323             The EID of the employee originating the request (needed for the audit triggers).
324              
325             =item * sql
326              
327             The SQL statement to execute (should be INSERT, UPDATE, or DELETE).
328              
329             =item * bind_params
330              
331             An array reference containing the bind values to be plugged into the SQL
332             statement.
333              
334             =back
335              
336             Returns a status object.
337              
338             Important note: it is up to the programmer to not pass any SQL statement that
339             might affect more than one record.
340              
341             =cut
342              
343             sub cud_generic {
344 0     0 1 0 my %ARGS = validate( @_, {
345             conn => { isa => 'DBIx::Connector' },
346             eid => { type => SCALAR },
347             sql => { type => SCALAR },
348             bind_params => { type => ARRAYREF, optional => 1 }, # order must match SQL statement
349             } );
350 0         0 $log->info( "Entering " . __PACKAGE__ . "::cud_generic with" );
351 0         0 $log->info( "sql: $ARGS{sql}" );
352 0         0 $log->info( "bind_param: " . Dumper( $ARGS{bind_params} ) );
353              
354 0         0 my ( $status, $rv, $count );
355              
356             try {
357             local $SIG{__WARN__} = sub {
358 0         0 die @_;
359 0     0   0 };
360              
361             # start transaction
362             $ARGS{'conn'}->txn( fixup => sub {
363              
364             # get DBI db handle
365 0         0 my $dbh = shift;
366              
367             # set the dochazka.eid GUC session parameter
368 0         0 $dbh->do( $site->SQL_SET_DOCHAZKA_EID_GUC, undef, ( $ARGS{'eid'}+0 ) );
369              
370             # prepare the SQL statement and bind parameters
371 0         0 my $sth = $dbh->prepare( $ARGS{'sql'} );
372 0         0 my $counter = 0;
373             map {
374 0         0 $counter += 1;
375 0   0     0 $sth->bind_param( $counter, $_ || undef );
376 0         0 } @{ $ARGS{'bind_params'} };
  0         0  
377              
378             # execute the SQL statement
379 0         0 $rv = $sth->execute;
380 0         0 $log->debug( "cud_generic: DBI execute returned " . Dumper( $rv ) );
381 0 0       0 if ( $rv >= 1 ) {
    0          
    0          
382              
383             # count number of rows affected
384 0         0 $count = $sth->rows;
385              
386             } elsif ( $rv eq '0E0' ) {
387              
388             # no error, but no record returned either
389 0         0 $count = $sth->rows;
390              
391             } elsif ( $rv == -1 ) {
392             $status = $CELL->status_err(
393             'DOCHAZKA_CUD_UNKNOWN_NUMBER_OF_RECORDS_AFFECTED',
394 0         0 args => [ $sth->{'Statement'} ]
395             );
396             } else {
397 0         0 $status = $CELL->status_crit( 'DOCHAZKA_DBI_EXECUTE_WEIRDNESS' );
398             }
399 0         0 } );
400             } catch {
401 0     0   0 my $errmsg = $_;
402 0 0       0 if ( not defined( $errmsg ) ) {
403 0         0 $log->err( '$_ undefined in catch' );
404 0         0 $errmsg = '<NONE>';
405             }
406 0 0       0 if ( not defined( $status ) ) {
407 0         0 $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR',
408             args => [ $errmsg ],
409             DBI_return_value => $rv,
410             );
411             }
412 0         0 };
413              
414 0 0       0 if ( not defined( $status ) ) {
415 0         0 $status = $CELL->status_ok( 'DOCHAZKA_CUD_OK',
416             DBI_return_value => $rv,
417             count => $count,
418             );
419             }
420              
421 0         0 return $status;
422             }
423              
424              
425             =head2 decode_schedule_json
426              
427             Given JSON string representation of the schedule, return corresponding HASHREF.
428              
429             =cut
430              
431             sub decode_schedule_json {
432 0     0 1 0 my ( $json_str ) = @_;
433              
434 0 0       0 return unless $json_str;
435 0         0 return JSON->new->utf8->canonical(1)->decode( $json_str );
436             }
437              
438              
439             =head2 get_history
440              
441             This function takes a number of arguments. The first two are (1) a SCALAR
442             argument, which can be either 'priv' or 'schedule', and (2) a L<DBIx::Connector>
443             object.
444              
445             Following these there is a PARAMHASH which can have one or more of the
446             properties 'eid', 'nick', and 'tsrange'. At least one of { 'eid', 'nick' } must
447             be specified. If both are specified, the employee is determined according to
448             'eid'.
449              
450             The function returns the history of privilege level or schedule changes for
451             that employee over the given tsrange, or the entire history if no tsrange is
452             supplied.
453              
454             The return value will always be an L<App::CELL::Status|status> object.
455              
456             Upon success, the payload will be a reference to an array of history
457             objects. If nothing is found, the array will be empty. If there is a DBI error,
458             the payload will be undefined.
459              
460             =cut
461              
462             sub get_history {
463 0     0 1 0 my $t = shift; # 'priv' or 'sched'
464 0         0 my $conn = shift;
465 0         0 validate_pos( @_, 1, 1, 0, 0, 0, 0 );
466 0         0 my %ARGS = validate( @_, {
467             eid => { type => SCALAR, optional => 1 },
468             nick => { type => SCALAR, optional => 1 },
469             tsrange => { type => SCALAR|UNDEF, optional => 1 },
470             } );
471              
472 0         0 $log->debug("Entering get_history for $t - arguments: " . Dumper( \%ARGS ) );
473              
474 0         0 my ( $sql, $sk, $status, $result, $tsr );
475 0 0       0 if ( exists $ARGS{'nick'} ) {
476 0 0       0 $sql = ($t eq 'priv')
477             ? $site->SQL_PRIVHISTORY_SELECT_RANGE_BY_NICK
478             : $site->SQL_SCHEDHISTORY_SELECT_RANGE_BY_NICK;
479 0         0 $result->{'nick'} = $ARGS{'nick'};
480 0 0       0 $result->{'eid'} = $ARGS{'eid'} if exists $ARGS{'eid'};
481 0         0 $sk = $ARGS{'nick'};
482             }
483 0 0       0 if ( exists $ARGS{'eid'} ) {
484 0 0       0 $sql = ($t eq 'priv')
485             ? $site->SQL_PRIVHISTORY_SELECT_RANGE_BY_EID
486             : $site->SQL_SCHEDHISTORY_SELECT_RANGE_BY_EID;
487 0         0 $result->{'eid'} = $ARGS{'eid'};
488 0 0       0 $result->{'nick'} = $ARGS{'nick'} if exists $ARGS{'nick'};
489 0         0 $sk = $ARGS{'eid'};
490             }
491 0         0 $log->debug("sql == $sql");
492             $tsr = ( $ARGS{'tsrange'} )
493 0 0       0 ? $ARGS{'tsrange'}
494             : '[,)';
495 0         0 $result->{'tsrange'} = $tsr;
496 0         0 $log->debug("tsrange == $tsr");
497              
498 0 0 0     0 die "AAAAAAAAAAAHHHHH! Engulfed by the abyss" unless $sk and $sql and $tsr;
      0        
499              
500 0         0 $result->{'history'} = [];
501             try {
502             $conn->run( fixup => sub {
503 0         0 my $sth = $_->prepare( $sql );
504 0         0 $sth->execute( $sk, $tsr );
505 0         0 while( defined( my $tmpres = $sth->fetchrow_hashref() ) ) {
506 0         0 push @{ $result->{'history'} }, $tmpres;
  0         0  
507             }
508 0     0   0 } );
509             } catch {
510 0     0   0 $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
511 0         0 };
512 0 0       0 return $status if defined $status;
513              
514 0         0 my $counter = scalar @{ $result->{'history'} };
  0         0  
515 0 0       0 return ( $counter )
516             ? $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
517             args => [ $counter ], payload => $result, count => $counter )
518             : $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND',
519             payload => $result, count => $counter );
520             }
521              
522              
523             =head2 load
524              
525             Load a database record into an object based on an SQL statement and a set of
526             search keys. The search key must be an exact match: this function returns only
527             1 or 0 records. Call, e.g., like this:
528              
529             my $status = load(
530             conn => $conn,
531             class => __PACKAGE__,
532             sql => $site->DOCHAZKA_SQL_SOME_STATEMENT,
533             keys => [ 44 ]
534             );
535              
536             The status object will be one of the following:
537              
538             =over
539              
540             =item * 1 record found
541              
542             Level C<OK>, code C<DISPATCH_RECORDS_FOUND>, payload: object of type 'class'
543              
544             =item * 0 records found
545              
546             Level C<NOTICE>, code C<DISPATCH_NO_RECORDS_FOUND>, payload: none
547              
548             =item * Database error
549              
550             Level C<ERR>, code C<DOCHAZKA_DBI_ERR>, text: error message, payload: none
551              
552             =back
553              
554             =cut
555              
556             sub load {
557             # get and verify arguments
558 0     0 1 0 my %ARGS = validate( @_, {
559             conn => { isa => 'DBIx::Connector' },
560             class => { type => SCALAR },
561             sql => { type => SCALAR },
562             keys => { type => ARRAYREF },
563             } );
564              
565             # consult the database; N.B. - select may only return a single record
566 0         0 my ( $hr, $status );
567             try {
568             $ARGS{'conn'}->run( fixup => sub {
569 0         0 $hr = $_->selectrow_hashref( $ARGS{'sql'}, undef, @{ $ARGS{'keys'} } );
  0         0  
570 0     0   0 } );
571             } catch {
572 0     0   0 $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
573 0         0 };
574              
575             # report the result
576 0 0       0 return $status if $status;
577             return $CELL->status_ok( 'DISPATCH_RECORDS_FOUND', args => [ '1' ],
578 0 0       0 payload => $ARGS{'class'}->spawn( %$hr ), count => 1 ) if defined $hr;
579 0         0 return $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND', count => 0 );
580             }
581              
582              
583             =head2 load_multiple
584              
585             Load multiple database records based on an SQL statement and a set of search
586             keys. Example:
587              
588             my $status = load_multiple(
589             conn => $conn,
590             class => __PACKAGE__,
591             sql => $site->DOCHAZKA_SQL_SOME_STATEMENT,
592             keys => [ 'rom%' ]
593             );
594              
595             The return value will be a status object, the payload of which will be an
596             arrayref containing a set of objects. The objects are constructed by calling
597             $ARGS{'class'}->spawn
598              
599             For convenience, a 'count' property will be included in the status object.
600              
601             =cut
602              
603             sub load_multiple {
604             # get and verify arguments
605 0     0 1 0 my %ARGS = validate( @_, {
606             conn => { isa => 'DBIx::Connector' },
607             class => { type => SCALAR },
608             sql => { type => SCALAR },
609             keys => { type => ARRAYREF },
610             } );
611 0         0 $log->debug( "Entering " . __PACKAGE__ . "::load_multiple" );
612              
613 0         0 my $status;
614 0         0 my $results = [];
615             try {
616             $ARGS{'conn'}->run( fixup => sub {
617 0         0 my $sth = $_->prepare( $ARGS{'sql'} );
618 0         0 my $bc = 0;
619             map {
620 0         0 $bc += 1;
621 0   0     0 $sth->bind_param( $bc, $_ || undef );
622 0         0 } @{ $ARGS{'keys'} };
  0         0  
623 0         0 $sth->execute();
624             # assuming they are objects, spawn them and push them onto @results
625 0         0 while( defined( my $tmpres = $sth->fetchrow_hashref() ) ) {
626 0         0 push @$results, $ARGS{'class'}->spawn( %$tmpres );
627             }
628 0     0   0 } );
629             } catch {
630 0     0   0 $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
631 0         0 };
632 0 0       0 return $status if defined $status;
633              
634 0         0 my $counter = scalar @$results;
635             $status = ( $counter )
636             ? $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
637 0 0       0 args => [ $counter ], payload => $results, count => $counter, keys => $ARGS{'keys'} )
638             : $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND',
639             payload => $results, count => $counter );
640             #$log->debug( Dumper $status );
641 0         0 return $status;
642             }
643              
644              
645             =head2 make_test_exists
646              
647             Returns coderef for a function, 'test_exists', that performs a simple
648             true/false check for existence of a record matching a scalar search key. The
649             record must be an exact match (no wildcards).
650              
651             Takes one argument: a type string C<$t> which is concatenated with the string
652             'load_by_' to arrive at the name of the function to be called to execute the
653             search.
654              
655             The returned function takes a single argument: the search key (a scalar value).
656             If a record matching the search key is found, the corresponding object
657             (i.e. a true value) is returned. If such a record does not exist, 'undef' (a
658             false value) is returned. If there is a DBI error, the error text is logged
659             and undef is returned.
660              
661             =cut
662              
663             sub make_test_exists {
664              
665 410     410 1 5764 my ( $t ) = validate_pos( @_, { type => SCALAR } );
666 410         2249 my $pkg = (caller)[0];
667              
668             return sub {
669 0     0     my ( $conn, $s_key ) = @_;
670 0           require Try::Tiny;
671 0           my $routine = "load_by_$t";
672 0           my ( $status, $txt );
673 0           $log->debug( "Entered $t" . "_exists with search key $s_key" );
674             try {
675 41     41   492 no strict 'refs';
  41         193  
  41         73522  
676 0     0     $status = $pkg->$routine( $conn, $s_key );
677             } catch {
678 0     0     $txt = "Function " . $pkg . "::test_exists was generated with argument $t, " .
679             "so it tried to call $routine, resulting in exception $_";
680 0           $status = $CELL->status_crit( $txt );
681 0           };
682 0 0 0       if ( ! defined( $status ) or $status->level eq 'CRIT' ) {
683 0           die $txt;
684             }
685             #$log->debug( "Status is " . Dumper( $status ) );
686 0 0         return $status->payload if $status->ok;
687 0           return;
688             }
689 410         3385 }
690              
691              
692             =head2 noof
693              
694             Given a L<DBIx::Connector> object and the name of a data model table, returns
695             the total number of records in the table.
696              
697             activities employees intervals locks privhistory schedhistory
698             schedintvls schedules tempintvls
699              
700             On failure, returns undef.
701              
702             =cut
703              
704             sub noof {
705 0     0 1   my ( $conn, $table ) = validate_pos( @_,
706             { isa => 'DBIx::Connector' },
707             { type => SCALAR }
708             );
709              
710 0 0         return unless grep { $table eq $_; } qw( activities employees intervals locks
  0            
711             privhistory schedhistory schedintvls schedules tempintvls );
712              
713 0           my $count;
714             try {
715             $conn->run( fixup => sub {
716 0           ( $count ) = $_->selectrow_array( "SELECT count(*) FROM $table" );
717 0     0     } );
718             } catch {
719 0     0     $CELL->status_crit( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
720 0           };
721 0           return $count;
722             }
723              
724              
725             =head2 priv_by_eid
726              
727             Given an EID, and, optionally, a timestamp, returns the employee's priv
728             level as of that timestamp, or as of "now" if no timestamp was given. The
729             priv level will default to 'passerby' if it can't be determined from the
730             database.
731              
732             =cut
733              
734             sub priv_by_eid {
735 0     0 1   my ( $conn, $eid, $ts ) = validate_pos( @_,
736             { isa => 'DBIx::Connector' },
737             { type => SCALAR },
738             { type => SCALAR|UNDEF, optional => 1 }
739             );
740             #$log->debug( "priv_by_eid: EID is " . (defined( $eid ) ? $eid : 'undef') . " - called from " . (caller)[1] . " line " . (caller)[2] );
741 0           return _st_by_eid( $conn, 'priv', $eid, $ts );
742             }
743              
744              
745             =head2 schedule_by_eid
746              
747             Given an EID, and, optionally, a timestamp, returns the SID of the employee's
748             schedule as of that timestamp, or as of "now" if no timestamp was given.
749              
750             =cut
751              
752             sub schedule_by_eid {
753 0     0 1   my ( $conn, $eid, $ts ) = validate_pos( @_,
754             { isa => 'DBIx::Connector' },
755             { type => SCALAR },
756             { type => SCALAR|UNDEF, optional => 1 },
757             );
758 0           return _st_by_eid( $conn, 'schedule', $eid, $ts );
759             }
760              
761              
762             =head3 _st_by_eid
763              
764             Function that 'priv_by_eid' and 'schedule_by_eid' are wrappers of.
765              
766             =cut
767              
768             sub _st_by_eid {
769 0     0     my ( $conn, $st, $eid, $ts ) = @_;
770 0           my ( @args, $sql, $row );
771 0   0       $log->debug( "Entering _st_by_eid with \$st == $st, \$eid == $eid, \$ts == " . ( $ts || '<NONE>' ) );
772 0 0         if ( $ts ) {
773             # timestamp given
774 0 0         if ( $st eq 'priv' ) {
    0          
775 0           $sql = $site->SQL_EMPLOYEE_PRIV_AT_TIMESTAMP;
776             } elsif ( $st eq 'schedule' ) {
777 0           $sql = $site->SQL_EMPLOYEE_SCHEDULE_AT_TIMESTAMP;
778             }
779 0           @args = ( $sql, undef, $eid, $ts );
780             } else {
781             # no timestamp given
782 0 0         if ( $st eq 'priv' ) {
    0          
783 0           $sql = $site->SQL_EMPLOYEE_CURRENT_PRIV;
784             } elsif ( $st eq 'schedule' ) {
785 0           $sql = $site->SQL_EMPLOYEE_CURRENT_SCHEDULE;
786             }
787 0           @args = ( $sql, undef, $eid );
788             }
789              
790 0           $log->debug("About to run SQL statement $sql with parameter $eid - " .
791             " called from " . (caller)[1] . " line " . (caller)[2] );
792              
793 0           my $status;
794             try {
795             $conn->run( fixup => sub {
796 0           ( $row ) = $_->selectrow_array( @args );
797 0     0     } );
798             } catch {
799 0     0     $log->debug( 'Encountered DBI error' );
800 0           $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
801 0           };
802 0 0         return $status if $status;
803              
804 0           $log->debug( "_st_by_eid success; returning payload " . Dumper( $row ) );
805 0           return $row;
806             }
807              
808              
809             =head2 select_single
810              
811             Given a L<DBIx::Connector> object in the 'conn' property, a SELECT statement in
812             the 'sql' property and, in the 'keys' property, an arrayref containing a list
813             of scalar values to plug into the SELECT statement, run a C<selectrow_array>
814             and return the resulting list.
815              
816             Returns a standard status object (see C<load> routine, above, for description).
817              
818             =cut
819              
820             sub select_single {
821 0     0 1   my %ARGS = validate( @_, {
822             conn => { isa => 'DBIx::Connector' },
823             sql => { type => SCALAR },
824             keys => { type => ARRAYREF },
825             } );
826 0           my ( $status, @results );
827 0           $log->info( "select_single keys: " . Dumper( $ARGS{keys} ) );
828             try {
829             $ARGS{'conn'}->run( fixup => sub {
830 0           @results = $_->selectrow_array( $ARGS{'sql'}, undef, @{ $ARGS{'keys'} } );
  0            
831 0     0     } );
832 0 0         my $count = scalar( @results ) ? 1 : 0;
833 0           $log->info( "count: $count" );
834 0 0         $status = ( $count )
835             ? $CELL->status_ok( 'DISPATCH_RECORDS_FOUND',
836             args => [ $count ], count => $count, payload => \@results )
837             : $CELL->status_notice( 'DISPATCH_NO_RECORDS_FOUND' );
838             } catch {
839 0     0     $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
840 0           };
841 0 0         die "AAAAHAHAAHAAAAAAGGGH! " . __PACKAGE__ . "::select_single" unless $status;
842 0           return $status;
843             }
844              
845              
846             =head2 select_set_of_single_scalar_rows
847              
848             Given DBIx::Connector object, an SQL statement, and a set of keys to bind
849             into the SQL statement, assume that the statement can return 0-n records
850             and that each record consists of a single field that must fit into a single
851             scalar value.
852              
853             =cut
854              
855             sub select_set_of_single_scalar_rows {
856 0     0 1   my %ARGS = validate( @_, {
857             conn => { isa => 'DBIx::Connector' },
858             sql => { type => SCALAR },
859             keys => { type => ARRAYREF },
860             } );
861 0           $log->debug( "Entering " . __PACKAGE__ . "::select_set_of_single_scalar_rows with
862             paramhash " . Dumper( \%ARGS ) );
863              
864 0           my ( $status, $result_set );
865             try {
866             $ARGS{'conn'}->run( fixup => sub {
867 0           my $sth = $_->prepare( $ARGS{'sql'} );
868 0           my $bc = 0;
869             map {
870 0           $bc += 1;
871 0   0       $sth->bind_param( $bc, $_ || undef );
872 0           } @{ $ARGS{'keys'} };
  0            
873 0           $sth->execute();
874             # push results onto $nicks
875 0           while( defined( my $tmpres = $sth->fetchrow_arrayref() ) ) {
876 0           push @$result_set, @$tmpres;
877             }
878 0     0     } );
879             } catch {
880 0     0     $log->debug( 'Encountered DBI error' );
881 0           $status = $CELL->status_err( 'DOCHAZKA_DBI_ERR', args => [ $_ ] );
882 0           };
883              
884 0 0         return $status if $status;
885 0           return $CELL->status_ok( 'RESULT_SET', payload => $result_set );
886             }
887              
888              
889             =head2 split_tsrange
890              
891             Given a string that might be a tsrange, run it through the database
892             using the SQL statement:
893              
894             SELECT lower(CAST( ? AS tstzrange )), upper(CAST( ? AS tstzrange ))
895              
896             If all goes well, the result will be an array ( from, to ) of two
897             timestamps.
898              
899             Returns a status object.
900              
901             =cut
902              
903             sub split_tsrange {
904 0     0 1   my ( $conn, $tsr ) = @_;
905              
906 0           my $status = select_single(
907             conn => $conn,
908             sql => 'SELECT lower(CAST( ? AS tstzrange )), upper(CAST( ? AS tstzrange ))',
909             keys => [ $tsr, $tsr ],
910             );
911 0 0         return $status unless $status->ok;
912 0           my ( $lower, $upper ) = @{ $status->payload };
  0            
913 0 0 0       return $CELL->status_err( 'DOCHAZKA_UNBOUNDED_TSRANGE' ) unless defined( $lower ) and
      0        
      0        
914             defined( $upper ) and $lower ne 'infinity' and $upper ne 'infinity';
915 0           return $status;
916             }
917              
918              
919             =head2 timestamp_delta_minus
920              
921             Given a timestamp string and an interval string (e.g. "1 week 3 days" ),
922             subtract the interval from the timestamp.
923              
924             Returns a status object. If the database operation is successful, the payload
925             will contain the resulting timestamp.
926              
927             =cut
928              
929             sub timestamp_delta_minus {
930 0     0 1   my ( $conn, $ts, $delta ) = validate_pos( @_,
931             { isa => 'DBIx::Connector' },
932             { type => SCALAR },
933             { type => SCALAR },
934             );
935 0           $log->info( "timestamp_delta_minus: timestamp $ts, delta $delta" );
936 0           my $status = select_single(
937             conn => $conn,
938             sql => "SELECT CAST( ? AS timestamptz ) - CAST( ? AS interval )",
939             keys => [ $ts, $delta ],
940             );
941 0 0         if ( $status->ok ) {
942 0           my ( $result ) = @{ $status->payload };
  0            
943 0           return $CELL->status_ok( 'SUCCESS', payload => $result );
944             }
945 0           return $status;
946             }
947              
948              
949             =head2 timestamp_delta_plus
950              
951             Given a timestamp string and an interval string (e.g. "1 week 3 days" ),
952             add the interval to the timestamp.
953              
954             Returns a status object. If the database operation is successful, the payload
955             will contain the resulting timestamp.
956              
957             =cut
958              
959             sub timestamp_delta_plus {
960 0     0 1   my ( $conn, $ts, $delta ) = validate_pos( @_,
961             { isa => 'DBIx::Connector' },
962             { type => SCALAR },
963             { type => SCALAR },
964             );
965 0           $log->info( "timestamp_delta_plus: timestamp $ts, delta $delta" );
966 0           my $status = select_single(
967             conn => $conn,
968             sql => "SELECT CAST( ? AS timestamptz ) + CAST( ? AS interval )",
969             keys => [ $ts, $delta ],
970             );
971 0 0         if ( $status->ok ) {
972 0           my ( $result ) = @{ $status->payload };
  0            
973 0           return $CELL->status_ok( 'SUCCESS', payload => $result );
974             }
975 0           return $status;
976             }
977              
978              
979             =head2 tsrange_intersection
980              
981             Given two strings that might be tsranges, consult the database and return
982             the result of tsrange1 * tsrange2 (also a tsrange).
983              
984             =cut
985              
986             sub tsrange_intersection {
987 0     0 1   my ( $conn, $tr1, $tr2 ) = @_;
988              
989 0           my $status = select_single(
990             conn => $conn,
991             sql => 'SELECT CAST( ? AS tstzrange) * CAST( ? AS tstzrange )',
992             keys => [ $tr1, $tr2 ],
993             );
994 0 0         die $status->text unless $status->ok;
995 0           return $status->payload->[0];
996             }
997              
998              
999             =head2 tsrange_equal
1000              
1001             Given two strings that might be equal tsranges, consult the database and return
1002             the result (true or false).
1003              
1004             =cut
1005              
1006             sub tsrange_equal {
1007 0     0 1   my ( $conn, $tr1, $tr2 ) = @_;
1008              
1009 0           my $status = select_single(
1010             conn => $conn,
1011             sql => 'SELECT CAST( ? AS tstzrange) = CAST( ? AS tstzrange )',
1012             keys => [ $tr1, $tr2 ],
1013             );
1014 0 0         die $status->text unless $status->ok;
1015 0           return $status->payload->[0];
1016             }
1017              
1018              
1019              
1020             =head1 AUTHOR
1021              
1022             Nathan Cutler, C<< <presnypreklad@gmail.com> >>
1023              
1024             =cut
1025              
1026             1;
1027