File Coverage

blib/lib/Couch/DB/Client.pm
Criterion Covered Total %
statement 30 240 12.5
branch 0 78 0.0
condition 0 26 0.0
subroutine 10 70 14.2
pod 27 28 96.4
total 67 442 15.1


line stmt bran cond sub pod time code
1             # This code is part of Perl distribution Couch-DB version 0.201.
2             # The POD got stripped from this file by OODoc version 3.06.
3             # For contributors see file ChangeLog.
4              
5             # This software is copyright (c) 2024-2026 by Mark Overmeer.
6              
7             # This is free software; you can redistribute it and/or modify it under
8             # the same terms as the Perl 5 programming language system itself.
9             # SPDX-License-Identifier: Artistic-1.0-Perl OR GPL-1.0-or-later
10              
11              
12             package Couch::DB::Client;{
13             our $VERSION = '0.201';
14             }
15              
16              
17 1     1   1481 use warnings;
  1         2  
  1         92  
18 1     1   8 use strict;
  1         3  
  1         35  
19              
20 1     1   6 use Couch::DB::Util qw/flat/;
  1         2  
  1         10  
21 1     1   7 use Couch::DB::Result ();
  1         3  
  1         49  
22              
23 1     1   7 use Log::Report 'couch-db';
  1         2  
  1         7  
24              
25 1     1   359 use Scalar::Util qw/weaken blessed/;
  1         2  
  1         68  
26 1     1   7 use List::Util qw/first/;
  1         2  
  1         90  
27 1     1   7 use MIME::Base64 qw/encode_base64/;
  1         2  
  1         89  
28 1     1   27 use Storable qw/dclone/;
  1         3  
  1         71  
29 1     1   8 use URI::Escape qw/uri_escape/;
  1         2  
  1         5344  
30              
31             my $seqnr = 0;
32              
33             #--------------------
34              
35 0     0 1   sub new(@) { (bless {}, shift)->init( {@_} ) }
36              
37             sub init($)
38 0     0 0   { my ($self, $args) = @_;
39 0 0         $self->{CDC_server} = my $server = delete $args->{server} or panic "Requires 'server'";
40 0   0       $self->{CDC_name} = delete $args->{name} || "$server";
41 0 0         $self->{CDC_ua} = delete $args->{user_agent} or panic "Requires 'user_agent'";
42 0           $self->{CDC_uuids} = [];
43 0           $self->{CDC_seqnr} = ++$seqnr;
44              
45 0 0         $self->{CDC_couch} = delete $args->{couch} or panic "Requires 'couch'";
46 0           weaken $self->{CDC_couch};
47              
48 0   0       $self->{CDC_hdrs} = my $headers = delete $args->{headers} || {};
49              
50 0   0       my $username = delete $args->{username} // '';
51             $self->login(
52             auth => delete $args->{auth} || 'BASIC',
53             username => $username,
54             password => delete $args->{password},
55 0 0 0       ) if length $username;
56              
57 0           $self;
58             }
59              
60             #--------------------
61              
62 0     0 1   sub name() { $_[0]->{CDC_name} }
63              
64              
65 0     0 1   sub couch() { $_[0]->{CDC_couch} }
66              
67              
68 0     0 1   sub server() { $_[0]->{CDC_server} }
69              
70              
71 0     0 1   sub userAgent() { $_[0]->{CDC_ua} }
72              
73              
74 0     0 1   sub headers($) { $_[0]->{CDC_hdrs} }
75              
76              
77 0     0 1   sub seqnr() { $_[0]->{CDC_seqnr} }
78              
79             #--------------------
80              
81             sub _clientIsMe($) # check no client parameter is used
82 0     0     { my ($self, $args) = @_;
83 0 0         defined $args->{client} and panic "No parameter 'client' allowed.";
84 0 0 0       $args->{clients} && @{delete $args->{clients}} and panic "No parameter 'clients' allowed.";
  0            
85 0           $args->{client} = $self;
86             }
87              
88             sub login(%)
89 0     0 1   { my ($self, %args) = @_;
90 0           $self->_clientIsMe(\%args);
91              
92 0   0       my $auth = delete $args{auth} || 'BASIC';
93 0 0         my $username = delete $args{username} or panic "Requires username";
94 0 0         my $password = delete $args{password} or panic "Requires password";
95              
96 0 0         if($auth eq 'BASIC')
97 0           { $self->headers->{Authorization} = 'Basic ' . encode_base64("$username:$password", '');
98 0           return $self; #XXX must return Result object
99             }
100              
101 0 0         $auth eq 'COOKIE'
102             or error __x"Unsupport authorization '{how}'", how => $auth;
103              
104             my $send = $self->{CDC_login} = # keep for cookie refresh (uninplemented)
105 0           +{name => $username, password => $password };
106              
107             $self->couch->call(POST => '/_session',
108             send => $send,
109             query => { next => delete $args{next} },
110             $self->couch->_resultsConfig(\%args, on_final => sub {
111 0 0   0     $self->{CDC_roles} = $_[0]->isReady ? $_[0]->values->{roles} : undef;
112 0           }),
113             );
114             }
115              
116              
117             sub session(%)
118 0     0 1   { my ($self, %args) = @_;
119 0           $self->_clientIsMe(\%args);
120 0           my $couch = $self->couch;
121              
122 0           my %query;
123 0 0         $query{basic} = delete $args{basic} if exists $args{basic};
124 0           $couch->toQuery(\%query, bool => qw/basic/);
125              
126             $couch->call(GET => '/_session',
127             query => \%query,
128             $couch->_resultsConfig(\%args, on_final => sub {
129 0 0   0     $self->{CDC_roles} = $_[0]->isReady ? $_[0]->values->{userCtx}{roles} : undef; $_[0];
  0            
130 0           }),
131             );
132             }
133              
134              
135             sub logout(%)
136 0     0 1   { my ($self, %args) = @_;
137 0           $self->_clientIsMe(\%args);
138              
139 0           $self->couch->call(DELETE => '/_session',
140             $self->couch->_resultsConfig(\%args),
141             );
142             }
143              
144              
145             sub roles()
146 0     0 1   { my $self = shift;
147 0 0         $self->{CDC_roles} or $self->session(basic => 1); # produced as side-effect
148 0 0         @{$self->{CDC_roles} || []};
  0            
149             }
150              
151              
152 0     0 1   sub hasRole($) { first { $_[1] eq $_ } $_[0]->roles }
  0     0      
153              
154             #--------------------
155              
156             sub __serverInfoValues($$)
157 0     0     { my ($self, $result, $data) = @_;
158 0           my $values = { %$data };
159              
160             # 3.3.3 does not contain the vendor/version, as the example in the spec says
161             # Probably a mistake.
162 0           $result->couch->toPerl($values, version => qw/version/);
163 0           $values;
164             }
165              
166             sub serverInfo(%)
167 0     0 1   { my ($self, %args) = @_;
168 0           $self->_clientIsMe(\%args);
169              
170 0   0       my $cached = delete $args{cached} || 'YES';
171 0 0         $cached =~ m!^(?:YES|NEVER|RETRY|PING)$! or panic "Unsupported cached parameter '$cached'.";
172              
173 0 0         if(my $result = $self->{CDC_info})
174             { return $self->{CDC_info}
175 0 0 0       if $cached eq 'YES' || ($cached eq 'RETRY' && $result->isReady);
      0        
176             }
177              
178             my $result = $self->couch->call(GET => '/',
179             $self->couch->_resultsConfig(\%args,
180 0     0     on_values => sub { $self->__serverInfoValues(@_) }
181 0           ),
182             );
183              
184 0 0         if($cached ne 'PING')
185 0           { $self->{CDC_info} = $result;
186 0           delete $self->{CDC_version};
187             }
188              
189 0           $result;
190             }
191              
192              
193              
194             sub version()
195 0     0 1   { my $self = shift;
196 0 0         return $self->{CDC_version} if exists $self->{CDC_version};
197              
198 0           my $result = $self->serverInfo(cached => 'YES');
199 0 0         $result->isReady or return undef;
200              
201             my $version = $result->values->{version}
202 0 0         or error __x"Server info field does not contain the server version.";
203              
204 0           $self->{CDC_version} = $version;
205             }
206              
207              
208             sub __simpleArrayRow($$%)
209 0     0     { my ($self, $result, $index, %args) = @_;
210 0 0         my $answer = $result->answer->[$index] or return ();
211              
212 0           ( answer => $answer,
213             values => $result->values->[$index],
214             );
215             }
216              
217             sub __activeTasksValues($$)
218 0     0     { my ($self, $result, $tasks) = @_;
219 0           my $couch = $result->couch;
220              
221 0           my @tasks;
222 0           foreach my $task (@$tasks)
223 0           { my %task = %$task;
224 0           $couch->toPerl(\%task, epoch => qw/started_on updated_on/);
225 0           push @tasks, \%task;
226             }
227              
228 0           \@tasks;
229             }
230              
231             sub activeTasks(%)
232 0     0 1   { my ($self, %args) = @_;
233 0           $self->_clientIsMe(\%args);
234              
235             $self->couch->call(GET => '/_active_tasks',
236             $self->couch->_resultsConfig(\%args,
237 0     0     on_values => sub { $self->__activeTasksValues(@_) },
238 0     0     on_row => sub { $self->__simpleArrayRow(@_) },
239 0           ),
240             );
241             }
242              
243              
244             sub __dbNamesFilter($)
245 0     0     { my ($self, $search) = @_;
246              
247 0 0         my $query = defined $search ? +{ %$search } : return {};
248 0           $self->couch
249             ->toQuery($query, bool => qw/descending/)
250             ->toQuery($query, json => qw/endkey end_key startkey start_key/);
251 0           $query;
252             }
253              
254             sub databaseNames(;$%)
255 0     0 1   { my ($self, $search, %args) = @_;
256 0           $self->_clientIsMe(\%args);
257              
258             $self->couch->call(GET => '/_all_dbs',
259             query => $self->__dbNamesFilter($search),
260             $self->couch->_resultsConfig(\%args,
261 0     0     on_row => sub { $self->__simpleArrayRow(@_) },
262 0           ),
263             );
264             }
265              
266              
267             sub databaseInfo(;$%)
268 0     0 1   { my ($self, $search, %args) = @_;
269 0           $self->_clientIsMe(\%args);
270 0           my $names = delete $args{names};
271              
272 0 0         my ($method, $query, $send, $intro) = $names
273             ? (POST => undef, +{ keys => $names }, '2.2.0')
274             : (GET => $self->_dbNamesFilter($search), undef, '3.2.0');
275              
276             $self->couch->call($method => '/_dbs_info',
277             introduced => $intro,
278             query => $query,
279             send => $send,
280             $self->couch->_resultsConfig(\%args,
281 0     0     on_row => sub { $self->__simpleArrayRow(@_) },
282 0           ),
283             );
284             }
285              
286              
287             sub __dbUpRow($$%)
288 0     0     { my ($self, $result, $index, %args) = @_;
289 0 0         my $answer = $result->answer->{results}[$index] or return ();
290             ( answer => $answer,
291 0           values => $result->values->{results}[$index],
292             );
293             }
294              
295             sub dbUpdates($%)
296 0     0 1   { my ($self, $feed, %args) = @_;
297 0           $self->_clientIsMe(\%args);
298              
299 0           my $query = +{ %$feed };
300              
301             $self->couch->call(GET => '/_db_updates',
302             introduced => '1.4.0',
303             query => $query,
304             $self->couch->_resultsConfig(\%args,
305 0     0     on_row => sub { $self->__dbUpRow(@_) },
306 0           ),
307             );
308             }
309              
310              
311             sub __clusterNodeValues($$)
312 0     0     { my ($self, $result, $data) = @_;
313 0           my $couch = $result->couch;
314              
315 0           my %values = %$data;
316 0           foreach my $set (qw/all_nodes cluster_nodes/)
317 0 0         { my $v = $values{$set} or next;
318 0           $values{$set} = [ $couch->listToPerl($set, node => $v) ];
319             }
320              
321 0           \%values;
322             }
323              
324             sub clusterNodes(%)
325 0     0 1   { my ($self, %args) = @_;
326 0           $self->_clientIsMe(\%args);
327              
328             $self->couch->call(GET => '/_membership',
329             introduced => '2.0.0',
330             $self->couch->_resultsConfig(\%args,
331 0     0     on_values => sub { $self->__clusterNodeValues(@_) }
332 0           ),
333             );
334             }
335              
336              
337             sub __replicateValues($$)
338 0     0     { my ($self, $result, $raw) = @_;
339 0           my $couch = $result->couch;
340              
341 0 0         my $history = delete $raw->{history} or return $raw;
342 0           my %values = %$raw;
343 0           my @history;
344              
345 0           foreach my $event (@$history)
346 0           { my %event = %$event;
347 0           $couch->toPerl(\%event, mailtime => qw/start_time end_time/);
348 0           push @history, \%event;
349             }
350 0           $values{history} = \@history;
351              
352 0           \%values;
353             }
354              
355             sub replicate($%)
356 0     0 1   { my ($self, $rules, %args) = @_;
357 0           $self->_clientIsMe(\%args);
358              
359 0           my $couch = $self->couch;
360 0           $couch->toJSON($rules, bool => qw/cancel continuous create_target winning_revs_only/);
361              
362             #TODO: warn for upcoming changes in source and target: absolute URLs required
363              
364             $couch->call(POST => '/_replicate',
365             send => $rules,
366             $couch->_resultsConfig(\%args,
367 0     0     on_values => sub { $self->__replicateValues(@_) }
368 0           ),
369             );
370             }
371              
372              
373             sub __replJobsRow($$%)
374 0     0     { my ($self, $result, $index, %args) = @_;
375 0 0         my $answer = $result->answer->{jobs}[$index] or return ();
376              
377             ( answer => $answer,
378 0           values => $result->values->{jobs}[$index],
379             );
380             }
381              
382             sub __replJobsValues($$)
383 0     0     { my ($self, $result, $raw) = @_;
384 0           my $couch = $result->couch;
385 0           my $values = dclone $raw;
386              
387 0 0         foreach my $job (@{$values->{jobs} || []})
  0            
388             {
389             $couch->toPerl($_, isotime => qw/timestamp/)
390 0 0         foreach @{$job->{history} || []};
  0            
391              
392 0           $couch->toPerl($job, isotime => qw/start_time/)
393             ->toPerl($job, abs_url => qw/target source/)
394             ->toPerl($job, node => qw/node/);
395             }
396              
397 0           $values;
398             }
399              
400             sub replicationJobs(%)
401 0     0 1   { my ($self, %args) = @_;
402 0           $self->_clientIsMe(\%args);
403              
404             $self->couch->call(GET => '/_scheduler/jobs',
405             $self->couch->_resultsPaging(\%args,
406 0     0     on_values => sub { $self->__replJobsValues(@_) },
407 0     0     on_row => sub { $self->__replJobsRow(@_) },
408 0           ),
409             );
410             }
411              
412              
413             sub __replDocRow($$%)
414 0     0     { my ($self, $result, $index, %args) = @_;
415 0 0         my $answer = $result->answer->{jobs}[$index] or return ();
416              
417             ( answer => $answer,
418 0           values => $result->values->{jobs}[$index],
419             );
420             }
421              
422             sub __replDocValues($$)
423 0     0     { my ($self, $result, $raw) = @_;
424 0           my $v = +{ %$raw }; # $raw->{info} needs no conversions
425              
426 0           $result->couch
427             ->toPerl($v, isotime => qw/start_time last_updated/)
428             ->toPerl($v, abs_url => qw/target source/)
429             ->toPerl($v, node => qw/node/);
430 0           $v;
431             }
432              
433             sub __replDocsValues($$)
434 0     0     { my ($self, $result, $raw) = @_;
435 0           my $couch = $result->couch;
436 0           my $values = dclone $raw;
437 0 0         $values->{docs} = [ map $self->__replDocValues($result, $_), @{$values->{docs} || []} ];
  0            
438 0           $values;
439             }
440              
441             sub replicationDocs(%)
442 0     0 1   { my ($self, %args) = @_;
443 0           $self->_clientIsMe(\%args);
444 0   0       my $dbname = delete $args{dbname} || '_replicator';
445              
446 0           my $path = '/_scheduler/docs';
447 0 0         if($dbname ne '_replicator')
448 0           { $path .= '/' . uri_escape($dbname);
449             }
450              
451             $self->couch->call(GET => $path,
452             $self->couch->_resultsPaging(\%args,
453 0     0     on_values => sub { $self->__replDocsValues(@_) },
454 0     0     on_row => sub { $self->__replDocRow(@_) },
455 0           ),
456             );
457             }
458              
459              
460             #XXX the output differs from replicationDoc, so different method
461              
462             sub __replOneDocValues($$)
463 0     0     { my ($self, $result, $raw) = @_;
464 0           $self->__replDocValues($result, $raw);
465             }
466              
467             sub replicationDoc($%)
468 0     0 1   { my ($self, $doc, %args) = @_;
469 0           $self->_clientIsMe(\%args);
470              
471 0   0       my $dbname = delete $args{dbname} || '_replicator';
472 0 0         my $docid = blessed $doc ? $doc->id : $doc;
473              
474 0           my $path = '/_scheduler/docs/' . uri_escape($dbname) . '/' . $docid;
475              
476             $self->couch->call(GET => $path,
477             $self->couch->_resultsConfig(\%args,
478 0     0     on_values => sub { $self->__replOneDocValues(@_) },
479 0           ),
480             );
481             }
482              
483              
484             sub __nodeNameValues($)
485 0     0     { my ($self, $result, $raw) = @_;
486 0           my $values = dclone $raw;
487 0           $result->couch->toPerl($values, node => qw/name/);
488 0           $values;
489             }
490              
491             sub nodeName($%)
492 0     0 1   { my ($self, $name, %args) = @_;
493 0           $self->_clientIsMe(\%args);
494              
495             $self->couch->call(GET => "/_node/$name",
496             $self->couch->_resultsConfig(\%args,
497 0     0     on_values => sub { $self->__nodeNameValues(@_) }
498 0           ),
499             );
500             }
501              
502              
503             sub node()
504 0     0 1   { my $self = shift;
505 0 0         return $self->{CDC_node} if defined $self->{CDC_node};
506              
507 0           my $result = $self->nodeName('_local', client => $self);
508 0 0         $result->isReady or return undef; # (temporary?) failure
509              
510 0 0         my $name = $result->value('name')
511             or error __x"did not get a node name for _local.";
512              
513 0           $self->{CDC_node} = $self->couch->node($name);
514             }
515              
516              
517             sub serverStatus(%)
518 0     0 1   { my ($self, %args) = @_;
519 0           $self->_clientIsMe(\%args);
520              
521 0           $self->couch->call(GET => '/_up',
522             introduced => '2.0.0',
523             $self->couch->_resultsConfig(\%args),
524             );
525             }
526              
527              
528             sub serverIsUp()
529 0     0 1   { my $self = shift;
530 0           my $result = $self->serverStatus;
531 0 0         $result && $result->answer->{status} eq 'ok';
532             }
533              
534             1;