File Coverage

blib/lib/Yancy/Backend.pm
Criterion Covered Total %
statement 12 67 17.9
branch 0 24 0.0
condition 0 19 0.0
subroutine 4 24 16.6
pod 19 20 95.0
total 35 154 22.7


line stmt bran cond sub pod time code
1             package Yancy::Backend;
2             our $VERSION = '1.088';
3             # ABSTRACT: Interface to a database
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod my $be = Yancy::Backend->new( $url );
8             #pod
9             #pod $result = $be->list( $schema, $where, $options );
10             #pod say "Total: " . $result->{total};
11             #pod say "Name: " . $_->{name} for @{ $result->{items} };
12             #pod
13             #pod $item = $be->get( $schema, $id );
14             #pod $be->set( $schema, $id, $item );
15             #pod $be->delete( $schema, $id );
16             #pod $id = $be->create( $schema, $item );
17             #pod
18             #pod =head1 DESCRIPTION
19             #pod
20             #pod A C handles talking to the database. Different Yancy
21             #pod backends will support different databases. To use a backend, see
22             #pod L. To make your own backend, see L for
23             #pod the list of methods each backend supports, their arguments, and their
24             #pod return values.
25             #pod
26             #pod =head2 Terminology
27             #pod
28             #pod Yancy backends work with schemas, which are made up of items.
29             #pod A schema is a set of items, like a database table. An item is
30             #pod a single element of a schema, and must be a hashref.
31             #pod
32             #pod =head2 Asynchronous Backends
33             #pod
34             #pod Asynchronous backends implement both a synchronous and an asynchronous
35             #pod API (using promises).
36             #pod
37             #pod =head2 Synchronous-only Backends
38             #pod
39             #pod Synchronous-only backends also implement a promises API for
40             #pod compatibility, but will not perform requests concurrently.
41             #pod
42             #pod =head1 SUPPORTED BACKENDS
43             #pod
44             #pod =over
45             #pod
46             #pod =item * L - Postgres backend
47             #pod
48             #pod =item * L - MySQL backend
49             #pod
50             #pod =item * L - SQLite backend
51             #pod
52             #pod =item * L - L backend
53             #pod
54             #pod =back
55             #pod
56             #pod Other backends are available on CPAN.
57             #pod
58             #pod =over
59             #pod
60             #pod =item * L - Backend for a static site generator
61             #pod with Markdown.
62             #pod
63             #pod =back
64             #pod
65             #pod =head1 EXTENDING
66             #pod
67             #pod To create your own Yancy::Backend for a new database system, inherit
68             #pod from this class and provide the standard six interface methods: L,
69             #pod L, L, L, L, and L.
70             #pod
71             #pod There are roles to aid backend development:
72             #pod
73             #pod =over
74             #pod
75             #pod =item * L provides some methods based on
76             #pod L
77             #pod
78             #pod =item * L provides promise-based API methods
79             #pod for databases which are synchronous-only.
80             #pod
81             #pod =back
82             #pod
83             #pod Backends do not have to talk to databases. For an example, see
84             #pod L for a backend that uses plain files like
85             #pod a static site generator.
86             #pod
87             #pod =cut
88              
89 1     1   2650 use Mojo::Base '-base';
  1         4  
  1         10  
90 1     1   253 use Scalar::Util qw( blessed );
  1         3  
  1         86  
91 1     1   7 use Yancy::Util qw( is_type is_format );
  1         2  
  1         51  
92 1     1   5 use Mojo::JSON qw( encode_json );
  1         2  
  1         1460  
93              
94             has schema => sub { {} };
95             sub collections {
96 0     0 0   require Carp;
97 0           Carp::carp( '"collections" method is now "schema"' );
98 0           shift->schema( @_ );
99             }
100              
101             #pod =head1 METHODS
102             #pod
103             #pod =head2 new
104             #pod
105             #pod my $url = 'memory://custom_string';
106             #pod my $be = Yancy::Backend::Memory->new( $url, $schema );
107             #pod
108             #pod Create a new backend object. C<$url> is a string that begins with the
109             #pod backend name followed by a colon. Everything else in the URL is for the
110             #pod backend to use to describe how to connect to the underlying database and
111             #pod any options for the backend object itself.
112             #pod
113             #pod The backend name will be run through C before being looked up
114             #pod in C. For example, C will use the
115             #pod L module.
116             #pod
117             #pod C<$schema> is a hash reference of schema configuration from the Yancy
118             #pod configuration. See L for more information.
119             #pod
120             #pod =cut
121              
122             sub new {
123 0     0 1   my ( $class, $driver, $schema ) = @_;
124 0 0         if ( $class eq __PACKAGE__ ) {
125 0           return load_backend( $driver, $schema );
126             }
127 0   0       return $class->SUPER::new( driver => $driver, schema => $schema // {} );
128             }
129              
130             #pod =head2 list
131             #pod
132             #pod my $result = $be->list( $schema, $where, %opt );
133             #pod # { total => ..., items => [ ... ] }
134             #pod
135             #pod Fetch a list of items from a schema. C<$schema> is the
136             #pod schema name.
137             #pod
138             #pod C<$where> is a L
139             #pod CLAUSES>.
140             #pod
141             #pod # Search for all Dougs
142             #pod $be->list( 'people', { name => { -like => 'Doug%' } } );
143             #pod # Find adults
144             #pod $be->list( 'people', { age => { '>=' => 18 } } );
145             #pod # Find men we can contact
146             #pod $be->list( 'people', { gender => 'male', contact => 1 } );
147             #pod
148             #pod Additionally, Yancy backends support the following additional
149             #pod keys in the where structure:
150             #pod
151             #pod =over
152             #pod
153             #pod =item -has (EXPERIMENTAL)
154             #pod
155             #pod The C<-has> operator searches inside a data structure (an array or
156             #pod a hash). This operator examines the type of the field being searched to
157             #pod perform the appropriate query.
158             #pod
159             #pod # Create a new page with an array of tags and a hash of author
160             #pod # information
161             #pod $be->create( pages => {
162             #pod title => 'Release v1.481',
163             #pod tags => [ 'release', 'minor' ],
164             #pod author => {
165             #pod name => 'Doug Bell',
166             #pod email => 'doug@example.com',
167             #pod },
168             #pod } );
169             #pod
170             #pod # All pages that have the tag "release"
171             #pod $be->list( pages => { tags => { -has => 'release' } } );
172             #pod
173             #pod # All pages that have both the tags "release" and "major"
174             #pod $be->list( pages => { tags => { -has => [ 'release', 'major' ] } } );
175             #pod
176             #pod # All pages that have the author's name starting with Doug
177             #pod $be->list( pages => { author => { -has => { name => { -like => 'Doug%' } } } } );
178             #pod
179             #pod This is not yet supported by all backends, and may never be supported by
180             #pod some. Postgres has array columns and JSON fields. MySQL has JSON fields.
181             #pod The L function matches against Perl data structures.
182             #pod All of these should support C<-has> and C<-not_has> before it can be
183             #pod considered not experimental.
184             #pod
185             #pod =back
186             #pod
187             #pod C<%opt> is a list of name/value pairs with the following keys:
188             #pod
189             #pod =over
190             #pod
191             #pod =item * limit - The number of items to return
192             #pod
193             #pod =item * offset - The number of items to skip
194             #pod
195             #pod =item * order_by - A L
196             #pod
197             #pod =item * join - Join one or more tables using a C field.
198             #pod This can be the name of a foreign key field on this schema, or the name
199             #pod of a table with a foreign key field that refers to this schema. Join
200             #pod multiple tables at the same time by passing an arrayref of joins. Fields
201             #pod in joined tables can be queried by prefixing the join name to the field,
202             #pod separated by a dot.
203             #pod
204             #pod =back
205             #pod
206             #pod # Get the second page of 20 people
207             #pod $be->list( 'people', {}, limit => 20, offset => 20 );
208             #pod # Get the list of people sorted by age, oldest first
209             #pod $be->list( 'people', {}, order_by => { -desc => 'age' } );
210             #pod # Get the list of people sorted by age first, then name (ascending)
211             #pod $be->list( 'people', {}, order_by => [ 'age', 'name' ] );
212             #pod
213             #pod Returns a hashref with two keys:
214             #pod
215             #pod =over
216             #pod
217             #pod =item items
218             #pod
219             #pod An array reference of hash references of item data
220             #pod
221             #pod =item total
222             #pod
223             #pod The total count of items that would be returned without C or
224             #pod C.
225             #pod
226             #pod =back
227             #pod
228             #pod =cut
229              
230 0     0 1   sub list { ... }
231              
232             #pod =head2 list_p
233             #pod
234             #pod my $promise = $be->list_p( $schema, $where, %opt );
235             #pod $promise->then( sub {
236             #pod my ( $result ) = @_;
237             #pod # { total => ..., items => [ ... ] }
238             #pod } );
239             #pod
240             #pod Fetch a list of items asynchronously using promises. Returns a promise that
241             #pod resolves to a hashref with C and C keys. See L for
242             #pod arguments and return values.
243             #pod
244             #pod =cut
245              
246 0     0 1   sub list_p { ... }
247              
248             #pod =head2 get
249             #pod
250             #pod my $item = $be->get( $schema, $id, %opts );
251             #pod
252             #pod Get a single item. C<$schema> is the schema name. C<$id> is the
253             #pod ID of the item to get: Either a string for a single key field, or a
254             #pod hash reference for a composite key. Returns a hashref of item data.
255             #pod
256             #pod C<%opts> is a list of name/value pairs of options with the following
257             #pod names:
258             #pod
259             #pod =over
260             #pod
261             #pod =item join
262             #pod
263             #pod Join one or more tables using a C field. This should be
264             #pod the name of the schema to join, either by a foreign key on this table,
265             #pod or a foreign key on the joined table. Join multiple tables at the same
266             #pod time by passing an arrayref of joins.
267             #pod
268             #pod =cut
269              
270 0     0 1   sub get { ... }
271              
272             #pod =head2 get_p
273             #pod
274             #pod my $promise = $be->get_p( $schema, $id );
275             #pod $promise->then( sub {
276             #pod my ( $item ) = @_;
277             #pod # ...
278             #pod } );
279             #pod
280             #pod Get a single item asynchronously using promises. Returns a promise that
281             #pod resolves to the item. See L for arguments and return values.
282             #pod
283             #pod =cut
284              
285 0     0 1   sub get_p { ... }
286              
287             #pod =head2 set
288             #pod
289             #pod my $success = $be->set( $schema, $id, $item );
290             #pod
291             #pod Update an item. C<$schema> is the schema name. C<$id> is the ID of the
292             #pod item to update: Either a string for a single key field, or a hash
293             #pod reference for a composite key. C<$item> is the item's data to set.
294             #pod Returns a boolean that is true if a row with the given ID was found and
295             #pod updated, false otherwise.
296             #pod
297             #pod Currently the values of the data cannot be references, only simple
298             #pod scalars or JSON booleans.
299             #pod
300             #pod =cut
301              
302 0     0 1   sub set { ... }
303              
304             #pod =head2 set_p
305             #pod
306             #pod my $promise = $be->set_p( $schema, $id );
307             #pod $promise->then( sub {
308             #pod my ( $success ) = @_;
309             #pod # ...
310             #pod } );
311             #pod
312             #pod Update a single item asynchronously using promises. Returns a promise
313             #pod that resolves to a boolean indicating if the row was updated. See
314             #pod L for arguments and return values.
315             #pod
316             #pod =cut
317              
318 0     0 1   sub set_p { ... }
319              
320             #pod =head2 create
321             #pod
322             #pod my $id = $be->create( $schema, $item );
323             #pod
324             #pod Create a new item. C<$schema> is the schema name. C<$item> is
325             #pod the item's data. Returns the ID of the row created suitable to be passed
326             #pod in to C.
327             #pod
328             #pod Currently the values of the data cannot be references, only simple
329             #pod scalars or JSON booleans.
330             #pod
331             #pod =cut
332              
333 0     0 1   sub create { ... }
334              
335             #pod =head2 create_p
336             #pod
337             #pod my $promise = $be->create_p( $schema, $item );
338             #pod $promise->then( sub {
339             #pod my ( $id ) = @_;
340             #pod # ...
341             #pod } );
342             #pod
343             #pod Create a new item asynchronously using promises. Returns a promise that
344             #pod resolves to the ID of the newly-created item. See L for
345             #pod arguments and return values.
346             #pod
347             #pod =cut
348              
349 0     0 1   sub create_p { ... }
350              
351             #pod =head2 delete
352             #pod
353             #pod $be->delete( $schema, $id );
354             #pod
355             #pod Delete an item. C<$schema> is the schema name. C<$id> is the ID of the
356             #pod item to delete: Either a string for a single key field, or a hash
357             #pod reference for a composite key. Returns a boolean that is true if a row
358             #pod with the given ID was found and deleted. False otherwise.
359             #pod
360             #pod =cut
361              
362 0     0 1   sub delete { ... }
363              
364             #pod =head2 delete_p
365             #pod
366             #pod my $promise = $be->delete_p( $schema, $id );
367             #pod $promise->then( sub {
368             #pod my ( $success ) = @_;
369             #pod # ...
370             #pod } );
371             #pod
372             #pod Delete an item asynchronously using promises. Returns a promise that
373             #pod resolves to a boolean indicating if the row was deleted. See L
374             #pod for arguments and return values.
375             #pod
376             #pod =cut
377              
378 0     0 1   sub delete_p { ... }
379              
380             #pod =head2 query
381             #pod
382             #pod Run a raw query on the backend. Each backend may have its own arguments.
383             #pod Returns a list of hash references of data.
384             #pod
385             #pod =cut
386              
387 0     0 1   sub query { ... }
388              
389             #pod =head2 query_p
390             #pod
391             #pod Run a raw query on the backend. Each backend may have its own arguments.
392             #pod Returns a promise that resolves into a list of hash references of data.
393             #pod
394             #pod =cut
395              
396 0     0 1   sub query_p { ... }
397              
398             #pod =head2 read_schema
399             #pod
400             #pod my $schema = $be->read_schema;
401             #pod my $table = $be->read_schema( $table_name );
402             #pod
403             #pod Read the schema from the database tables. Returns an OpenAPI schema
404             #pod ready to be merged into the user's configuration. Can be restricted
405             #pod to only a single table.
406             #pod
407             #pod =cut
408              
409 0     0 1   sub read_schema { ... }
410              
411             #pod =head1 INTERNAL METHODS
412             #pod
413             #pod These methods are documented for use in subclasses and should not need
414             #pod to be called externally.
415             #pod
416             #pod =head2 supports
417             #pod
418             #pod Returns true if the backend supports a given feature. Returns false for now.
419             #pod In the future, features like 'json' will be detectable.
420             #pod
421             #pod =cut
422              
423 0     0 1   sub supports { 0 }
424              
425             #pod =head2 ignore_table
426             #pod
427             #pod Returns true if the given table should be ignored when doing L.
428             #pod By default, backends will ignore tables used by:
429             #pod
430             #pod =over
431             #pod
432             #pod =item * L backends (L, L, L)
433             #pod =item * L
434             #pod =item * Mojo DB migrations (L, L, L)
435             #pod =item * L
436             #pod
437             #pod =back
438             #pod
439             #pod =cut
440              
441             our %IGNORE_TABLE = (
442             mojo_migrations => 1,
443             minion_jobs => 1,
444             minion_workers => 1,
445             minion_locks => 1,
446             minion_workers_inbox => 1,
447             minion_jobs_depends => 1,
448             mojo_pubsub_subscribe => 1,
449             mojo_pubsub_notify => 1,
450             dbix_class_schema_versions => 1,
451             );
452             sub ignore_table {
453 0   0 0 1   return $IGNORE_TABLE{ $_[1] } // 0;
454             }
455              
456             #pod =head2 normalize
457             #pod
458             #pod This method normalizes data to and from the database.
459             #pod
460             #pod =cut
461              
462             sub normalize {
463 0     0 1   my ( $self, $schema_name, $data ) = @_;
464 0 0         return undef if !$data;
465 0           my $schema = $self->schema->{ $schema_name };
466 0   0       my $real_schema_name = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      0        
467             my %props = %{
468 0           $schema->{properties} || $self->schema->{ $real_schema_name }{properties}
469 0 0         };
470 0           my %replace;
471 0           for my $key ( keys %$data ) {
472 0 0         next if !defined $data->{ $key }; # leave nulls alone
473 0   0       my $prop = $props{ $key } || next;
474 0           my ( $type, $format ) = @{ $prop }{qw( type format )};
  0            
475 0 0 0       if ( is_type( $type, 'boolean' ) ) {
    0          
476             # Boolean: true (1, "true"), false (0, "false")
477             $replace{ $key }
478 0 0 0       = $data->{ $key } && $data->{ $key } !~ /^false$/i
479             ? 1 : 0;
480             }
481             elsif ( is_type( $type, 'string' ) && is_format( $format, 'date-time' ) ) {
482 0 0         if ( !$data->{ $key } ) {
    0          
483 0           $replace{ $key } = undef;
484             }
485             elsif ( $data->{ $key } eq 'now' ) {
486 0           $replace{ $key } = \'CURRENT_TIMESTAMP';
487             }
488             else {
489 0           $replace{ $key } = $data->{ $key };
490 0           $replace{ $key } =~ s/T/ /;
491             }
492             }
493             }
494              
495 0           my $params = +{ %$data, %replace };
496 0           return $params;
497             }
498              
499             #pod =head2 id_field
500             #pod
501             #pod Get the ID field for the given schema. Defaults to C.
502             #pod =cut
503              
504             sub id_field {
505 0     0 1   my ( $self, $schema ) = @_;
506 0   0       return $self->schema->{ $schema }{ 'x-id-field' } || 'id';
507             }
508              
509             #pod =head2 id_where
510             #pod
511             #pod Get the query structure for the ID field of the given schema with the
512             #pod given ID value.
513             #pod
514             #pod =cut
515              
516             sub id_where {
517 0     0 1   my ( $self, $schema_name, $id ) = @_;
518 0           my %where;
519 0           my $id_field = $self->id_field( $schema_name );
520 0 0         if ( ref $id_field eq 'ARRAY' ) {
521 0           for my $field ( @$id_field ) {
522 0 0         next unless exists $id->{ $field };
523 0           $where{ $field } = $id->{ $field };
524             }
525 0 0         die "Missing composite ID parts" if @$id_field > keys %where;
526             }
527             else {
528 0           $where{ $id_field } = $id;
529             }
530 0           return %where;
531             }
532              
533             1;
534              
535             __END__