File Coverage

blib/lib/Yancy/Backend.pm
Criterion Covered Total %
statement 12 65 18.4
branch 0 24 0.0
condition 0 19 0.0
subroutine 4 22 18.1
pod 17 18 94.4
total 33 148 22.3


line stmt bran cond sub pod time code
1             package Yancy::Backend;
2             our $VERSION = '1.086';
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   647 use Mojo::Base '-base';
  1         2  
  1         6  
90 1     1   155 use Scalar::Util qw( blessed );
  1         2  
  1         80  
91 1     1   6 use Yancy::Util qw( is_type is_format );
  1         2  
  1         62  
92 1     1   6 use Mojo::JSON qw( encode_json );
  1         2  
  1         1168  
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 read_schema
381             #pod
382             #pod my $schema = $be->read_schema;
383             #pod my $table = $be->read_schema( $table_name );
384             #pod
385             #pod Read the schema from the database tables. Returns an OpenAPI schema
386             #pod ready to be merged into the user's configuration. Can be restricted
387             #pod to only a single table.
388             #pod
389             #pod =cut
390              
391 0     0 1   sub read_schema { ... }
392              
393             #pod =head1 INTERNAL METHODS
394             #pod
395             #pod These methods are documented for use in subclasses and should not need
396             #pod to be called externally.
397             #pod
398             #pod =head2 supports
399             #pod
400             #pod Returns true if the backend supports a given feature. Returns false for now.
401             #pod In the future, features like 'json' will be detectable.
402             #pod
403             #pod =cut
404              
405 0     0 1   sub supports { 0 }
406              
407             #pod =head2 ignore_table
408             #pod
409             #pod Returns true if the given table should be ignored when doing L.
410             #pod By default, backends will ignore tables used by:
411             #pod
412             #pod =over
413             #pod
414             #pod =item * L backends (L, L, L)
415             #pod =item * L
416             #pod =item * Mojo DB migrations (L, L, L)
417             #pod =item * L
418             #pod
419             #pod =back
420             #pod
421             #pod =cut
422              
423             our %IGNORE_TABLE = (
424             mojo_migrations => 1,
425             minion_jobs => 1,
426             minion_workers => 1,
427             minion_locks => 1,
428             minion_workers_inbox => 1,
429             minion_jobs_depends => 1,
430             mojo_pubsub_subscribe => 1,
431             mojo_pubsub_notify => 1,
432             dbix_class_schema_versions => 1,
433             );
434             sub ignore_table {
435 0   0 0 1   return $IGNORE_TABLE{ $_[1] } // 0;
436             }
437              
438             #pod =head2 normalize
439             #pod
440             #pod This method normalizes data to and from the database.
441             #pod
442             #pod =cut
443              
444             sub normalize {
445 0     0 1   my ( $self, $schema_name, $data ) = @_;
446 0 0         return undef if !$data;
447 0           my $schema = $self->schema->{ $schema_name };
448 0   0       my $real_schema_name = ( $schema->{'x-view'} || {} )->{schema} // $schema_name;
      0        
449             my %props = %{
450 0           $schema->{properties} || $self->schema->{ $real_schema_name }{properties}
451 0 0         };
452 0           my %replace;
453 0           for my $key ( keys %$data ) {
454 0 0         next if !defined $data->{ $key }; # leave nulls alone
455 0   0       my $prop = $props{ $key } || next;
456 0           my ( $type, $format ) = @{ $prop }{qw( type format )};
  0            
457 0 0 0       if ( is_type( $type, 'boolean' ) ) {
    0          
458             # Boolean: true (1, "true"), false (0, "false")
459             $replace{ $key }
460 0 0 0       = $data->{ $key } && $data->{ $key } !~ /^false$/i
461             ? 1 : 0;
462             }
463             elsif ( is_type( $type, 'string' ) && is_format( $format, 'date-time' ) ) {
464 0 0         if ( !$data->{ $key } ) {
    0          
465 0           $replace{ $key } = undef;
466             }
467             elsif ( $data->{ $key } eq 'now' ) {
468 0           $replace{ $key } = \'CURRENT_TIMESTAMP';
469             }
470             else {
471 0           $replace{ $key } = $data->{ $key };
472 0           $replace{ $key } =~ s/T/ /;
473             }
474             }
475             }
476              
477 0           my $params = +{ %$data, %replace };
478 0           return $params;
479             }
480              
481             #pod =head2 id_field
482             #pod
483             #pod Get the ID field for the given schema. Defaults to C.
484             #pod =cut
485              
486             sub id_field {
487 0     0 1   my ( $self, $schema ) = @_;
488 0   0       return $self->schema->{ $schema }{ 'x-id-field' } || 'id';
489             }
490              
491             #pod =head2 id_where
492             #pod
493             #pod Get the query structure for the ID field of the given schema with the
494             #pod given ID value.
495             #pod
496             #pod =cut
497              
498             sub id_where {
499 0     0 1   my ( $self, $schema_name, $id ) = @_;
500 0           my %where;
501 0           my $id_field = $self->id_field( $schema_name );
502 0 0         if ( ref $id_field eq 'ARRAY' ) {
503 0           for my $field ( @$id_field ) {
504 0 0         next unless exists $id->{ $field };
505 0           $where{ $field } = $id->{ $field };
506             }
507 0 0         die "Missing composite ID parts" if @$id_field > keys %where;
508             }
509             else {
510 0           $where{ $id_field } = $id;
511             }
512 0           return %where;
513             }
514              
515             1;
516              
517             __END__