File Coverage

blib/lib/LabKey/Query.pm
Criterion Covered Total %
statement 42 217 19.3
branch 0 72 0.0
condition 0 40 0.0
subroutine 14 27 51.8
pod 5 5 100.0
total 61 361 16.9


line stmt bran cond sub pod time code
1             #!/usr/bin/perl
2              
3             =head1 NAME
4              
5             LabKey::Query - For interacting with data in LabKey Server
6              
7             =head1 SYNOPSIS
8              
9             use LabKey::Query;
10             my $results = LabKey::Query::selectRows(
11             -baseUrl => 'http://labkey.com:8080/labkey/',
12             -containerPath => 'myFolder/',
13             -schemaName => 'lists',
14             -queryName => 'mid_tags',
15             );
16            
17             =head1 ABSTRACT
18              
19             For interacting with data in LabKey Server
20              
21             =head1 DESCRIPTION
22              
23             This module is designed to simplify querying and manipulating data in LabKey Server. It should more or less replicate the javascript APIs of the same names.
24              
25             After the module is installed, if you need to login with a specific user you
26             will need to create a L<.netrc|https://www.labkey.org/Documentation/wiki-page.view?name=netrc>
27             file in the home directory of the user running the perl script.
28              
29             In API versions 0.08 and later, you can specify the param '-loginAsGuest'
30             which will query the server without any credentials. The server must permit
31             guest to that folder for this to work though.
32              
33             =cut
34              
35             package LabKey::Query;
36              
37 6     6   413203 use warnings;
  6         57  
  6         248  
38 6     6   34 use strict;
  6         13  
  6         114  
39 6     6   3916 use JSON;
  6         74236  
  6         34  
40 6     6   4416 use Data::Dumper;
  6         38022  
  6         366  
41 6     6   2700 use FileHandle;
  6         59229  
  6         35  
42 6     6   1790 use File::Spec;
  6         11  
  6         169  
43 6     6   3669 use File::HomeDir;
  6         33107  
  6         304  
44 6     6   44 use Carp;
  6         13  
  6         327  
45              
46              
47             # Force all SSL connections to use TLSv1 or greater protocol. This is required for
48             # MacOSX and older Windows workstations.
49             #
50             # Credit to @chrisrth on stackoverflow (http://stackoverflow.com/a/20305596)
51             # See https://www.labkey.org/issues/home/Developer/issues/details.view?issueId=22146
52             # for more information.
53             #
54 6     6   4886 use IO::Socket::SSL;
  6         469225  
  6         58  
55             my $context = new IO::Socket::SSL::SSL_Context(
56             SSL_version => 'tlsv1'
57             );
58             IO::Socket::SSL::set_default_context($context);
59              
60 6     6   5968 use LWP::UserAgent;
  6         285095  
  6         223  
61 6     6   3053 use HTTP::Cookies;
  6         42492  
  6         263  
62 6     6   2899 use HTTP::Request::Common;
  6         13477  
  6         390  
63 6     6   46 use URI;
  6         15  
  6         166  
64              
65              
66 6     6   34 use vars qw($VERSION);
  6         19  
  6         20666  
67              
68             our $VERSION = "1.07";
69              
70              
71              
72             =head1 FUNCTIONS
73              
74             =head2 selectRows()
75              
76             selectRows() can be used to query data from LabKey server
77              
78             The following are the minimum required params:
79            
80             my $results = LabKey::Query::selectRows(
81             -baseUrl => 'http://labkey.com:8080/labkey/',
82             -containerPath => 'myFolder/',
83             -schemaName => 'lists',
84             -queryName => 'mid_tags',
85             );
86              
87             The following are optional:
88              
89             -viewName => 'view1',
90             -filterArray => [
91             ['file_active', 'eq', 1],
92             ['species', 'neq', 'zebra']
93             ], #allows filters to be applied to the query similar to the labkey Javascript API.
94             -parameters => [
95             ['enddate', '2011/01/01'],
96             ['totalDays', 15]
97             ], #allows parameters to be applied to the query similar to the labkey Javascript API.
98             -maxRows => 10 #the max number of rows returned
99             -sort => 'ColumnA,ColumnB' #sort order used for this query
100             -offset => 100 #the offset used when running the query
101             -columns => 'ColumnA,ColumnB' #A comma-delimited list of column names to include in the results.
102             -containerFilterName => 'currentAndSubfolders'
103             -debug => 1, #will result in a more verbose output
104             -loginAsGuest => #will not attempt to lookup credentials in netrc
105             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
106             -requiredVersion => 9.1 #if 8.3 is selected, it will use LabKey's pre-9.1 format for returning the data. 9.1 is the default. See documentation of LABKEY.Query.ExtendedSelectRowsResults for more detail here:
107             https://www.labkey.org/download/clientapi_docs/javascript-api/symbols/LABKEY.Query.html
108             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
109             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
110            
111             =head3 NOTE
112              
113             In version 1.0 and later of the perl API, the default result format is 9.1. This is different from the LabKey JS, which defaults to the earlier format for legacy purposes.
114              
115             =cut
116              
117             sub selectRows {
118 0     0 1   my %args = @_;
119              
120 0           my @required = ('-queryName', '-schemaName');
121 0           _checkRequiredParams(\%args, \@required);
122              
123 0           my $ctx = _getServerContext(%args);
124              
125             my $data = {
126             'schemaName' => $args{'-schemaName'},
127             'query.queryName' => $args{'-queryName'},
128 0   0       'apiVersion' => $args{'-requiredVersion'} || 9.1
129             };
130              
131 0           foreach (@{$args{-filterArray}}) {
  0            
132 0           $$data{'query.' . @{$_}[0] . '~' . @{$_}[1]} = @{$_}[2];
  0            
  0            
  0            
133             }
134              
135 0           foreach (@{$args{-parameters}}) {
  0            
136 0           $$data{'query.param.' . @{$_}[0]} = @{$_}[1];
  0            
  0            
137             }
138            
139 0           foreach ('viewName', 'offset', 'sort', 'maxRows', 'columns', 'containerFilterName') {
140 0 0         if ($args{'-' . $_}) {
141 0           $$data{'query.' . $_} = $args{'-' . $_};
142             }
143             }
144            
145 0           return _postData($ctx, _buildURL($ctx, 'query', 'getQuery.api'), $data);
146             }
147              
148              
149             =head2 insertRows()
150              
151             insertRows() can be used to insert records into a LabKey table
152              
153             The following are the minimum required params:
154              
155             my $insert = LabKey::Query::insertRows(
156             -baseUrl => 'http://labkey.com:8080/labkey/',
157             -containerPath => 'myFolder/',
158             -schemaName => 'lists',
159             -queryName => 'backup',
160             -rows => [{
161             "JobName" => 'jobName',
162             "Status" => $status,
163             "Log" => $log,
164             "Date" => $date
165             }],
166             );
167            
168             The following are optional:
169              
170             -debug => 1, #will result in a more verbose output
171             -loginAsGuest => #will not attempt to lookup credentials in netrc
172             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
173             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
174             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
175              
176             =cut
177              
178             sub insertRows {
179 0     0 1   my %args = @_;
180              
181 0           my @required = ('-queryName', '-schemaName', '-rows');
182 0           _checkRequiredParams(\%args, \@required);
183              
184 0           my $ctx = _getServerContext(%args);
185              
186             my $data = {
187             "schemaName" => $args{'-schemaName'},
188             "queryName" => $args{'-queryName'},
189 0           "rows" => $args{'-rows'}
190             };
191              
192 0           return _postData($ctx, _buildURL($ctx, 'query', 'insertRows.api'), $data);
193             }
194              
195              
196             =head2 updateRows()
197              
198             updateRows() can be used to update records in a LabKey table
199              
200             The following are the minimum required params:
201              
202             my $update = LabKey::Query::updateRows(
203             -baseUrl => 'http://labkey.com:8080/labkey/',
204             -containerPath => 'myFolder/',
205             -schemaName => 'lists',
206             -queryName => 'backup',
207             -rows => [{
208             "JobName" => 'jobName',
209             "Status" => $status,
210             "Log" => $log,
211             "Date" => $date
212             }],
213             );
214            
215             The following are optional:
216              
217             -debug => 1, #will result in a more verbose output
218             -loginAsGuest => #will not attempt to lookup credentials in netrc
219             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
220             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
221             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
222              
223             =cut
224              
225             sub updateRows {
226 0     0 1   my %args = @_;
227              
228 0           my @required = ('-queryName', '-schemaName', '-rows');
229 0           _checkRequiredParams(\%args, \@required);
230              
231 0           my $ctx = _getServerContext(%args);
232              
233             my $data = {
234             "schemaName" => $args{'-schemaName'},
235             "queryName" => $args{'-queryName'},
236 0           "rows" => $args{'-rows'}
237             };
238              
239 0           return _postData($ctx, _buildURL($ctx, 'query', 'updateRows.api'), $data);
240             }
241              
242              
243             =head2 deleteRows()
244              
245             deleteRows() can be used to delete records in a LabKey table
246              
247             The following are the minimum required params:
248              
249             my $update = LabKey::Query::deleteRows(
250             -baseUrl => 'http://labkey.com:8080/labkey/',
251             -containerPath => 'myFolder/',
252             -schemaName => 'lists',
253             -queryName => 'backup',
254             -rows => [{
255             "Key" => '12',
256             }],
257             );
258            
259             The following are optional:
260              
261             -debug => 1, #will result in a more verbose output
262             -loginAsGuest => #will not attempt to lookup credentials in netrc
263             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
264             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
265             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
266              
267             =cut
268              
269             sub deleteRows {
270 0     0 1   my %args = @_;
271              
272 0           my @required = ('-queryName', '-schemaName', '-rows');
273 0           _checkRequiredParams(\%args, \@required);
274              
275 0           my $ctx = _getServerContext(%args);
276              
277             my $data = {
278             "schemaName" => $args{'-schemaName'},
279             "queryName" => $args{'-queryName'},
280 0           "rows" => $args{'-rows'}
281             };
282              
283 0           return _postData($ctx, _buildURL($ctx, 'query', 'deleteRows.api'), $data);
284             }
285              
286              
287             =head2 executeSql()
288              
289             executeSql() can be used to execute arbitrary SQL
290              
291             The following are the minimum required params:
292              
293             my $result = LabKey::Query::executeSql(
294             -baseUrl => 'http://labkey.com:8080/labkey/',
295             -containerPath => 'myFolder/',
296             -schemaName => 'study',
297             -sql => 'select MyDataset.foo, MyDataset.bar from MyDataset',
298             );
299            
300             The following are optional:
301              
302             -maxRows => 10 #the max number of rows returned
303             -sort => 'ColumnA,ColumnB' #sort order used for this query
304             -offset => 100 #the offset used when running the query
305             -containerFilterName => 'currentAndSubfolders'
306             -debug => 1, #will result in a more verbose output
307             -loginAsGuest => #will not attempt to lookup credentials in netrc
308             -netrcFile => optional. the location of a file to use in place of a .netrc file. see also the environment variable LABKEY_NETRC.
309             -useragent => an instance of LWP::UserAgent (if not provided, a new instance will be created)
310             -timeout => timeout in seconds (used when creating a new LWP::UserAgent)
311              
312             =cut
313              
314             sub executeSql {
315 0     0 1   my %args = @_;
316              
317 0           my @required = ('-schemaName', '-sql');
318 0           _checkRequiredParams(\%args, \@required);
319              
320 0           my $ctx = _getServerContext(%args);
321              
322             my $data = {
323             "schemaName" => $args{'-schemaName'},
324 0           "sql" => $args{'-sql'}
325             };
326            
327 0           foreach ('offset', 'sort', 'maxRows', 'containerFilterName') {
328 0 0         if ($args{'-' . $_}) {
329 0           $$data{$_} = $args{'-' . $_};
330             }
331             }
332            
333 0           return _postData($ctx, _buildURL($ctx, 'query', 'executeSql.api'), $data);
334             }
335              
336              
337             # NOTE: this code adapted from Net::Netrc module. It was changed so alternate locations could be supplied for a .netrc file
338             sub _readrc {
339              
340 0   0 0     my $host = shift || 'default';
341 0           my $file = shift;
342              
343             #allow user to supply netrc location
344 0 0 0       if(!$file || !-e $file){
345 0           $file = File::Spec->catfile( File::HomeDir::home(), '.netrc' );
346             }
347 0 0         if ( !-e $file ) {
348 0           $file = File::Spec->catfile( File::HomeDir::home(), '_netrc' );
349             }
350              
351 0           my %netrc = ();
352 0           my $fh;
353 0           local $_;
354              
355 0           $netrc{default} = undef;
356              
357             # OS/2 and Win32 do not handle stat in a way compatable with this check :-(
358 0 0 0       unless ( $^O eq 'os2'
      0        
      0        
      0        
359             || $^O eq 'MSWin32'
360             || $^O eq 'MacOS'
361             || $^O eq 'darwin'
362             || $^O =~ /^cygwin/ )
363             {
364 0           my @stat = stat($file);
365              
366 0 0         if (@stat) {
367 0 0         if ( $stat[2] & 077 ) {
368 0           carp "Bad permissions: $file";
369 0           return;
370             }
371 0 0         if ( $stat[4] != $< ) {
372 0           carp "Not owner: $file";
373 0           return;
374             }
375             }
376             }
377              
378 0 0         if ( $fh = FileHandle->new( $file, "r" ) ) {
379 0           my ( $mach, $macdef, $tok, @tok ) = ( 0, 0 );
380              
381 0           while (<$fh>) {
382 0 0         undef $macdef if /\A\n\Z/;
383              
384 0 0         if ($macdef) {
385 0           push( @$macdef, $_ );
386 0           next;
387             }
388              
389 0           s/^\s*//;
390 0           chomp;
391              
392 0   0       while ( length && s/^("((?:[^"]+|\\.)*)"|((?:[^\\\s]+|\\.)*))\s*// )
393             {
394 0           ( my $tok = $+ ) =~ s/\\(.)/$1/g;
395 0           push( @tok, $tok );
396             }
397              
398             TOKEN:
399 0           while (@tok) {
400 0 0         if ( $tok[0] eq "default" ) {
401 0           shift(@tok);
402 0           $mach = bless {};
403 0           $netrc{default} = [$mach];
404              
405 0           next TOKEN;
406             }
407              
408             last TOKEN
409 0 0         unless @tok > 1;
410              
411 0           $tok = shift(@tok);
412              
413 0 0         if ( $tok eq "machine" ) {
    0          
    0          
414 0           my $host = shift @tok;
415 0           $mach = { machine => $host };
416              
417             $netrc{$host} = []
418 0 0         unless exists( $netrc{$host} );
419 0           push( @{ $netrc{$host} }, $mach );
  0            
420             }
421             elsif ( $tok =~ /^(login|password|account)$/ ) {
422 0 0         next TOKEN unless $mach;
423 0           my $value = shift @tok;
424              
425             # Following line added by rmerrell to remove '/' escape char in .netrc
426 0           $value =~ s/\/\\/\\/g;
427 0           $mach->{$1} = $value;
428             }
429             elsif ( $tok eq "macdef" ) {
430 0 0         next TOKEN unless $mach;
431 0           my $value = shift @tok;
432             $mach->{macdef} = {}
433 0 0         unless exists $mach->{macdef};
434 0           $macdef = $mach->{machdef}{$value} = [];
435             }
436             }
437             }
438 0           $fh->close();
439             }
440            
441 0           my $auth = $netrc{$host}[0];
442              
443             #if no machine is specified and there is only 1 machine in netrc, we use that one
444 0 0 0       if (!$auth && length((keys %netrc))==1){
445 0           $auth = $netrc{(keys %netrc)[0]}[0];
446             }
447              
448 0 0         warn("Unable to find entry for host: $host") unless $auth;
449 0 0         warn("Missing password for host: $host") unless $auth->{password};
450 0 0         warn("Missing login for host: $host") unless $auth->{login};
451              
452 0           return $auth;
453             }
454              
455              
456             sub _normalizeSlash {
457 0     0     my ($containerPath) = @_;
458            
459 0           $containerPath =~ s/^\///;
460 0           $containerPath =~ s/\/$//;
461 0           $containerPath .= '/';
462 0           return $containerPath;
463             }
464              
465              
466             sub _postData {
467 0     0     my ($ctx, $url, $data) = @_;
468              
469 0 0         print "POST " . $url . "\n" if $$ctx{debug};
470 0 0         print Dumper($data) if $$ctx{debug};
471              
472 0           my $json_obj = JSON->new->utf8->encode($data);
473              
474 0           my $req = POST $url;
475 0           $req->content_length(length($json_obj));
476 0           $req->content_type('application/json');
477 0           $req->content($json_obj);
478 0           $req->authorization_basic($$ctx{auth}{'login'}, $$ctx{auth}{'password'});
479              
480 0           my $response = $$ctx{userAgent}->request($req);
481              
482             # Simple error checking
483 0 0         if ( $response->is_error ) {
484 0           croak($response->status_line . '\n' . $response->decoded_content);
485             }
486              
487 0   0       my $response_json = JSON->new->utf8->decode( $response->content )
488             || croak("ERROR: Unable to decode JSON.\n$url\n");
489            
490 0           return $response_json;
491             }
492              
493             sub _createUserAgent {
494 0     0     my %args = @_;
495              
496 0           my $ua = LWP::UserAgent->new;
497 0           $ua->agent("Perl API Client/1.0");
498 0           $ua->cookie_jar(HTTP::Cookies->new());
499              
500 0 0         if ($args{'-timeout'}) {
501 0           print "setting timeout to " . $args{'-timeout'} . "\n";
502 0           $ua->timeout($args{'-timeout'});
503             }
504 0           return $ua;
505             }
506              
507             sub _buildURL {
508 0     0     my ($ctx, $controller, $action) = @_;
509              
510             return URI->new(
511             _normalizeSlash($$ctx{baseUrl})
512             . _normalizeSlash($controller)
513             . _normalizeSlash($$ctx{containerPath})
514 0           . $action
515             . '?'
516             );
517             }
518              
519             sub _checkRequiredParams {
520 0     0     my %args = %{$_[0]};
  0            
521 0           my @required = @{$_[1]};
  0            
522              
523 0           foreach (@required) {
524 0 0         if (!$args{$_}) {
525 0           croak("ERROR: Missing required param: $_")
526             }
527             }
528             }
529              
530             sub _fetchCSRF {
531 0     0     my ($ctx) = @_;
532              
533 0           my $url = _buildURL($ctx, 'login', 'whoAmI.api');
534              
535 0 0         print "CRSF " . $url . "\n" if $$ctx{debug};
536              
537 0           my $req = GET $url;
538 0           $req->content_type('application/json');
539              
540 0 0         if (!$$ctx{isGuest}) {
541 0           $req->authorization_basic($$ctx{auth}{'login'}, $$ctx{auth}{'password'});
542             }
543              
544 0           my $response = $$ctx{userAgent}->request($req);
545              
546 0 0         if ($response->is_error) {
547 0           croak($response->status_line . '\n' . $response->decoded_content);
548             }
549              
550 0   0       my $json_obj = JSON->new->utf8->decode($response->content)
551             || croak("ERROR: Unable to decode JSON.\n$url\n");
552              
553 0           return $$json_obj{'CSRF'};
554             }
555              
556             sub _getServerContext {
557 0     0     my %args = @_;
558              
559             #allow baseUrl as environment variable
560 0   0       $args{'-baseUrl'} = $args{'-baseUrl'} || $ENV{LABKEY_URL};
561              
562 0           my @required = ('-containerPath', '-baseUrl');
563 0           _checkRequiredParams(\%args, \@required);
564              
565             #if no machine supplied, extract domain from baseUrl
566 0 0         if (!$args{'-machine'}) {
567 0           $args{'-machine'} = URI->new($args{'-baseUrl'})->host;
568             }
569              
570 0           my $is_guest;
571             my $lk_config;
572 0   0       my $netrc_file = $args{-netrcFile} || $ENV{LABKEY_NETRC};
573              
574 0 0         if ($args{'-loginAsGuest'}) {
575 0           $is_guest = 1;
576             }
577             else {
578 0           $lk_config = _readrc($args{-machine}, $netrc_file);
579 0           $is_guest = 0;
580             }
581              
582             my $ctx = {
583             auth => $lk_config,
584             baseUrl => $args{'-baseUrl'},
585             containerPath => $args{'-containerPath'},
586             isGuest => $is_guest,
587 0   0       userAgent => $args{'-useragent'} || _createUserAgent(%args),
588             };
589              
590 0 0         if ($args{-debug}) {
591 0           $$ctx{debug} = 1;
592             }
593              
594 0           my $csrfHeader = "X-LABKEY-CSRF";
595              
596 0 0         if (!$$ctx{userAgent}->default_header($csrfHeader)) {
597 0           $$ctx{userAgent}->default_header($csrfHeader => _fetchCSRF($ctx));
598             }
599              
600 0           return $ctx;
601             }
602              
603             =pod
604              
605             =head1 ENVIORNMENT VARIABLES
606              
607             =over 4
608              
609             =item *
610             The 'LABKEY_URL' environment variable can be used instead of supplying a '-baseUrl' param.
611              
612             =item *
613             The 'LABKEY_NETRC' environment variable can be used to specify an alternate location of a netrc file, if not in the user's home directory.
614              
615             =back
616              
617             =head1 AUTHOR
618              
619             LabKey C
620              
621             =head1 CONTRIBUTING
622              
623             Send comments, suggestions and bug reports to:
624              
625             L
626              
627              
628             =head1 COPYRIGHT
629            
630             Copyright (c) 2010 Ben Bimber
631             Copyright (c) 2011-2020 LabKey Corporation
632              
633             =head1 LICENSE
634              
635             Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
636              
637             =head1 SEE ALSO
638              
639             The LabKey client APIs are described in greater detail here:
640             https://www.labkey.org/Documentation/wiki-page.view?name=viewAPIs
641              
642             Support questions should be directed to the LabKey forum:
643             https://www.labkey.org/home/Server/Forum/announcements-list.view
644              
645             =cut
646              
647             1;
648