File Coverage

blib/lib/Mojolicious/Plugin/Yancy.pm
Criterion Covered Total %
statement 200 210 95.2
branch 47 62 75.8
condition 29 39 74.3
subroutine 30 32 93.7
pod 1 1 100.0
total 307 344 89.2


line stmt bran cond sub pod time code
1             package Mojolicious::Plugin::Yancy;
2             our $VERSION = '1.088';
3             # ABSTRACT: Embed a simple admin CMS into your Mojolicious application
4              
5             #pod =head1 SYNOPSIS
6             #pod
7             #pod use Mojolicious::Lite;
8             #pod plugin Yancy => backend => 'sqlite:myapp.db'; # mysql, pg, dbic...
9             #pod app->start;
10             #pod
11             #pod =head1 DESCRIPTION
12             #pod
13             #pod This plugin allows you to add a simple content management system (CMS)
14             #pod to administrate content on your L site. This includes
15             #pod a JavaScript web application to edit the content and a REST API to help
16             #pod quickly build your own application.
17             #pod
18             #pod =head1 CONFIGURATION
19             #pod
20             #pod For getting started with a configuration for Yancy, see
21             #pod the L<"Yancy Guides"|Yancy::Guides>.
22             #pod
23             #pod Additional configuration keys accepted by the plugin are:
24             #pod
25             #pod =over
26             #pod
27             #pod =item backend
28             #pod
29             #pod In addition to specifying the backend as a single URL (see L<"Database
30             #pod Backend"|Yancy::Guides::Schema/Database Backend>), you can specify it as
31             #pod a hashref of C<< class => $db >>. This allows you to share database
32             #pod connections.
33             #pod
34             #pod use Mojolicious::Lite;
35             #pod use Mojo::Pg;
36             #pod helper pg => sub { state $pg = Mojo::Pg->new( 'postgres:///myapp' ) };
37             #pod plugin Yancy => { backend => { Pg => app->pg } };
38             #pod
39             #pod =item model
40             #pod
41             #pod (optional) Specify a model class or object that extends L.
42             #pod By default, will create a basic L object.
43             #pod
44             #pod plugin Yancy => { backend => { Pg => app->pg }, model => 'MyApp::Model' };
45             #pod
46             #pod my $model = Yancy::Model->with_roles( 'MyRole' );
47             #pod plugin Yancy => { backend => { Pg => app->pg }, model => $model };
48             #pod
49             #pod =item route
50             #pod
51             #pod A base route to add the Yancy editor to. This allows you to customize
52             #pod the URL and add authentication or authorization. Defaults to allowing
53             #pod access to the Yancy web application under C, and the REST API
54             #pod under C.
55             #pod
56             #pod This can be a string or a L object.
57             #pod
58             #pod # These are equivalent
59             #pod use Mojolicious::Lite;
60             #pod plugin Yancy => { route => app->routes->any( '/admin' ) };
61             #pod plugin Yancy => { route => '/admin' };
62             #pod
63             #pod =item return_to
64             #pod
65             #pod The URL to use for the "Back to Application" link. Defaults to C.
66             #pod
67             #pod =item filters
68             #pod
69             #pod A hash of C<< name => subref >> pairs of filters to make available.
70             #pod See L for how to create a filter subroutine.
71             #pod
72             #pod B Filters are deprecated and will be removed in Yancy v2. See
73             #pod L for a way to replace them.
74             #pod
75             #pod =back
76             #pod
77             #pod =head1 HELPERS
78             #pod
79             #pod This plugin adds some helpers for use in routes, templates, and plugins.
80             #pod
81             #pod =head2 yancy.config
82             #pod
83             #pod my $config = $c->yancy->config;
84             #pod
85             #pod The current configuration for Yancy. Through this, you can edit the
86             #pod C configuration as needed.
87             #pod
88             #pod =head2 yancy.backend
89             #pod
90             #pod my $be = $c->yancy->backend;
91             #pod
92             #pod Get the Yancy backend object. By default, gets the backend configured
93             #pod while loading the Yancy plugin. Requests can override the backend by
94             #pod setting the C stash value. See L for the
95             #pod methods you can call on a backend object and their purpose.
96             #pod
97             #pod =head2 yancy.plugin
98             #pod
99             #pod Add a Yancy plugin. Yancy plugins are Mojolicious plugins that require
100             #pod Yancy features and are found in the L namespace.
101             #pod
102             #pod use Mojolicious::Lite;
103             #pod plugin 'Yancy';
104             #pod app->yancy->plugin( 'Auth::Basic', { schema => 'users' } );
105             #pod
106             #pod You can also add the Yancy::Plugin namespace into the default plugin
107             #pod lookup locations. This allows you to treat them like any other
108             #pod Mojolicious plugin.
109             #pod
110             #pod # Lite app
111             #pod use Mojolicious::Lite;
112             #pod plugin 'Yancy', ...;
113             #pod unshift @{ app->plugins->namespaces }, 'Yancy::Plugin';
114             #pod plugin 'Auth::Basic', ...;
115             #pod
116             #pod # Full app
117             #pod use Mojolicious;
118             #pod sub startup {
119             #pod my ( $app ) = @_;
120             #pod $app->plugin( 'Yancy', ... );
121             #pod unshift @{ $app->plugins->namespaces }, 'Yancy::Plugin';
122             #pod $app->plugin( 'Auth::Basic', ... );
123             #pod }
124             #pod
125             #pod Yancy does not do this for you to avoid namespace collisions.
126             #pod
127             #pod =head2 yancy.model
128             #pod
129             #pod my $model = $c->yancy->model;
130             #pod my $schema = $c->yancy->model( $schema_name );
131             #pod
132             #pod Return the L or a L by name.
133             #pod
134             #pod =head2 yancy.list
135             #pod
136             #pod my @items = $c->yancy->list( $schema, \%param, \%opt );
137             #pod
138             #pod Get a list of items from the backend. C<$schema> is a schema
139             #pod name. C<\%param> is a L
140             #pod structure|SQL::Abstract/WHERE CLAUSES>. Some basic examples:
141             #pod
142             #pod # All people named exactly 'Turanga Leela'
143             #pod $c->yancy->list( people => { name => 'Turanga Leela' } );
144             #pod
145             #pod # All people with "Wong" in their name
146             #pod $c->yancy->list( people => { name => { like => '%Wong%' } } );
147             #pod
148             #pod C<\%opt> is a hash of options with the following keys:
149             #pod
150             #pod =over
151             #pod
152             #pod =item * limit - The number of items to return
153             #pod
154             #pod =item * offset - The number of items to skip before returning items
155             #pod
156             #pod =back
157             #pod
158             #pod See L
159             #pod method's arguments|Yancy::Backend/list>. This helper only returns the list
160             #pod of items, not the total count of items or any other value.
161             #pod
162             #pod This helper will also filter out any password fields in the returned
163             #pod data. To get all the data, use the L helper to
164             #pod access the backend methods directly.
165             #pod
166             #pod =head2 yancy.get
167             #pod
168             #pod my $item = $c->yancy->get( $schema, $id );
169             #pod
170             #pod Get an item from the backend. C<$schema> is the schema name.
171             #pod C<$id> is the ID of the item to get. See L.
172             #pod
173             #pod This helper will filter out password values in the returned data. To get
174             #pod all the data, use the L helper to access the
175             #pod backend directly.
176             #pod
177             #pod =head2 yancy.set
178             #pod
179             #pod $c->yancy->set( $schema, $id, $item_data, %opt );
180             #pod
181             #pod Update an item in the backend. C<$schema> is the schema name.
182             #pod C<$id> is the ID of the item to update. C<$item_data> is a hash of data
183             #pod to update. See L. C<%opt> is a list of options with
184             #pod the following keys:
185             #pod
186             #pod =over
187             #pod
188             #pod =item * properties - An arrayref of properties to validate, for partial updates
189             #pod
190             #pod =back
191             #pod
192             #pod This helper will validate the data against the configuration and run any
193             #pod filters as needed. If validation fails, this helper will throw an
194             #pod exception with an array reference of L objects.
195             #pod See L and L
196             #pod helper|/yancy.filter.apply>. To bypass filters and validation, use the
197             #pod backend object directly via L.
198             #pod
199             #pod # A route to update a comment
200             #pod put '/comment/:id' => sub {
201             #pod eval { $c->yancy->set( "comment", $c->stash( 'id' ), $c->req->json ) };
202             #pod if ( $@ ) {
203             #pod return $c->render( status => 400, errors => $@ );
204             #pod }
205             #pod return $c->render( status => 200, text => 'Success!' );
206             #pod };
207             #pod
208             #pod =head2 yancy.create
209             #pod
210             #pod my $item = $c->yancy->create( $schema, $item_data );
211             #pod
212             #pod Create a new item. C<$schema> is the schema name. C<$item_data>
213             #pod is a hash of data for the new item. See L.
214             #pod
215             #pod This helper will validate the data against the configuration and run any
216             #pod filters as needed. If validation fails, this helper will throw an
217             #pod exception with an array reference of L objects.
218             #pod See L and L
219             #pod helper|/yancy.filter.apply>. To bypass filters and validation, use the
220             #pod backend object directly via L.
221             #pod
222             #pod # A route to create a comment
223             #pod post '/comment' => sub {
224             #pod eval { $c->yancy->create( "comment", $c->req->json ) };
225             #pod if ( $@ ) {
226             #pod return $c->render( status => 400, errors => $@ );
227             #pod }
228             #pod return $c->render( status => 200, text => 'Success!' );
229             #pod };
230             #pod
231             #pod =head2 yancy.delete
232             #pod
233             #pod $c->yancy->delete( $schema, $id );
234             #pod
235             #pod Delete an item from the backend. C<$schema> is the schema name.
236             #pod C<$id> is the ID of the item to delete. See L.
237             #pod
238             #pod =head2 yancy.validate
239             #pod
240             #pod my @errors = $c->yancy->validate( $schema, $item, %opt );
241             #pod
242             #pod Validate the given C<$item> data against the configuration for the
243             #pod C<$schema>. If there are any errors, they are returned as an array
244             #pod of L objects. C<%opt> is a list of options with
245             #pod the following keys:
246             #pod
247             #pod =over
248             #pod
249             #pod =item * properties - An arrayref of properties to validate, for partial updates
250             #pod
251             #pod =back
252             #pod
253             #pod See L for more details.
254             #pod
255             #pod =head2 yancy.form
256             #pod
257             #pod By default, the L form plugin is
258             #pod loaded. You can override this with your own form plugin. See
259             #pod L for more information.
260             #pod
261             #pod =head2 yancy.file
262             #pod
263             #pod By default, the L plugin is loaded to handle file
264             #pod uploading and file management. The default path for file uploads is
265             #pod C<$MOJO_HOME/public/uploads>. You can override this with your own file
266             #pod plugin. See L for more information.
267             #pod
268             #pod =head2 yancy.filter.add
269             #pod
270             #pod B Filters are deprecated and will be removed in Yancy v2. See
271             #pod L for a way to replace them.
272             #pod
273             #pod my $filter_sub = sub { my ( $field_name, $field_value, $field_conf, @params ) = @_; ... }
274             #pod $c->yancy->filter->add( $name => $filter_sub );
275             #pod
276             #pod Create a new filter. C<$name> is the name of the filter to give in the
277             #pod field's configuration. C<$subref> is a subroutine reference that accepts
278             #pod at least three arguments:
279             #pod
280             #pod =over
281             #pod
282             #pod =item * $name - The name of the schema/field being filtered
283             #pod
284             #pod =item * $value - The value to filter, either the entire item, or a single field
285             #pod
286             #pod =item * $conf - The configuration for the schema/field
287             #pod
288             #pod =item * @params - Other parameters if configured
289             #pod
290             #pod =back
291             #pod
292             #pod For example, here is a filter that will run a password through a one-way hash
293             #pod digest:
294             #pod
295             #pod use Digest;
296             #pod my $digest = sub {
297             #pod my ( $field_name, $field_value, $field_conf ) = @_;
298             #pod my $type = $field_conf->{ 'x-digest' }{ type };
299             #pod Digest->new( $type )->add( $field_value )->b64digest;
300             #pod };
301             #pod $c->yancy->filter->add( 'digest' => $digest );
302             #pod
303             #pod And you configure this on a field using C<< x-filter >> and C<< x-digest >>:
304             #pod
305             #pod # mysite.conf
306             #pod {
307             #pod schema => {
308             #pod users => {
309             #pod properties => {
310             #pod username => { type => 'string' },
311             #pod password => {
312             #pod type => 'string',
313             #pod format => 'password',
314             #pod 'x-filter' => [ 'digest' ], # The name of the filter
315             #pod 'x-digest' => { # Filter configuration
316             #pod type => 'SHA-1',
317             #pod },
318             #pod },
319             #pod },
320             #pod },
321             #pod },
322             #pod }
323             #pod
324             #pod The same filter, but also configurable with extra parameters:
325             #pod
326             #pod my $digest = sub {
327             #pod my ( $field_name, $field_value, $field_conf, @params ) = @_;
328             #pod my $type = ( $params[0] || $field_conf->{ 'x-digest' } )->{ type };
329             #pod Digest->new( $type )->add( $field_value )->b64digest;
330             #pod $field_value . $params[0];
331             #pod };
332             #pod $c->yancy->filter->add( 'digest' => $digest );
333             #pod
334             #pod The alternative configuration:
335             #pod
336             #pod # mysite.conf
337             #pod {
338             #pod schema => {
339             #pod users => {
340             #pod properties => {
341             #pod username => { type => 'string' },
342             #pod password => {
343             #pod type => 'string',
344             #pod format => 'password',
345             #pod 'x-filter' => [ [ digest => { type => 'SHA-1' } ] ],
346             #pod },
347             #pod },
348             #pod },
349             #pod },
350             #pod }
351             #pod
352             #pod Schemas can also have filters. A schema filter will get the
353             #pod entire hash reference as its value. For example, here's a filter that
354             #pod updates the C field with the current time:
355             #pod
356             #pod $c->yancy->filter->add( 'timestamp' => sub {
357             #pod my ( $schema_name, $item, $schema_conf ) = @_;
358             #pod $item->{last_updated} = time;
359             #pod return $item;
360             #pod } );
361             #pod
362             #pod And you configure this on the schema using C<< x-filter >>:
363             #pod
364             #pod # mysite.conf
365             #pod {
366             #pod schema => {
367             #pod people => {
368             #pod 'x-filter' => [ 'timestamp' ],
369             #pod properties => {
370             #pod name => { type => 'string' },
371             #pod address => { type => 'string' },
372             #pod last_updated => { type => 'datetime' },
373             #pod },
374             #pod },
375             #pod },
376             #pod }
377             #pod
378             #pod You can configure filters on OpenAPI operations' inputs. These will
379             #pod probably want to operate on hash-refs as in the schema-level filters
380             #pod above. The config passed will be an empty hash. The filter can be applied
381             #pod to either or both of the path, or the individual operation, and will be
382             #pod executed in that order. E.g.:
383             #pod
384             #pod # mysite.conf
385             #pod {
386             #pod openapi => {
387             #pod definitions => {
388             #pod people => {
389             #pod properties => {
390             #pod name => { type => 'string' },
391             #pod address => { type => 'string' },
392             #pod last_updated => { type => 'datetime' },
393             #pod },
394             #pod },
395             #pod },
396             #pod paths => {
397             #pod "/people" => {
398             #pod # could also have x-filter here
399             #pod "post" => {
400             #pod 'x-filter' => [ 'timestamp' ],
401             #pod # ...
402             #pod },
403             #pod },
404             #pod }
405             #pod },
406             #pod }
407             #pod
408             #pod You can also configure filters on OpenAPI operations' outputs, this time
409             #pod with the key C. Again, the config passed will be an empty
410             #pod hash. The filter can be applied to either or both of the path, or the
411             #pod individual operation, and will be executed in that order. E.g.:
412             #pod
413             #pod # mysite.conf
414             #pod {
415             #pod openapi => {
416             #pod paths => {
417             #pod "/people" => {
418             #pod 'x-filter-output' => [ 'timestamp' ],
419             #pod # ...
420             #pod },
421             #pod }
422             #pod },
423             #pod }
424             #pod
425             #pod =head3 Supplied filters
426             #pod
427             #pod These filters are always installed.
428             #pod
429             #pod =head4 yancy.from_helper
430             #pod
431             #pod The first configured parameter is the name of an installed Mojolicious
432             #pod helper. That helper will be called, with any further supplied parameters,
433             #pod and the return value will be used as the value of that field /
434             #pod item. E.g. with this helper:
435             #pod
436             #pod $app->helper( 'current_time' => sub { scalar gmtime } );
437             #pod
438             #pod This configuration will achieve the same as the above with C:
439             #pod
440             #pod # mysite.conf
441             #pod {
442             #pod schema => {
443             #pod people => {
444             #pod properties => {
445             #pod name => { type => 'string' },
446             #pod address => { type => 'string' },
447             #pod last_updated => {
448             #pod type => 'datetime',
449             #pod 'x-filter' => [ [ 'yancy.from_helper' => 'current_time' ] ],
450             #pod },
451             #pod },
452             #pod },
453             #pod },
454             #pod }
455             #pod
456             #pod =head4 yancy.overlay_from_helper
457             #pod
458             #pod Intended to be used for "items" rather than individual fields, as it
459             #pod will only work when the "value" parameter is a hash-ref.
460             #pod
461             #pod The configured parameters are supplied in pairs. The first item in the
462             #pod pair is the string key in the hash-ref. The second is either the name of
463             #pod a helper, or an array-ref with the first entry as such a helper-name,
464             #pod followed by parameters to pass that helper. For each pair, the helper
465             #pod will be called, and its return value set as the relevant key's value.
466             #pod E.g. with this helper:
467             #pod
468             #pod $app->helper( 'current_time' => sub { scalar gmtime } );
469             #pod
470             #pod This configuration will achieve the same as the above with C:
471             #pod
472             #pod # mysite.conf
473             #pod {
474             #pod schema => {
475             #pod people => {
476             #pod 'x-filter' => [
477             #pod [ 'yancy.overlay_from_helper' => 'last_updated', 'current_time' ]
478             #pod ],
479             #pod properties => {
480             #pod name => { type => 'string' },
481             #pod address => { type => 'string' },
482             #pod last_updated => { type => 'datetime' },
483             #pod },
484             #pod },
485             #pod },
486             #pod }
487             #pod
488             #pod =head4 yancy.wrap
489             #pod
490             #pod The configured parameters are a list of strings. For each one, the
491             #pod original value will be wrapped in a hash with that string as the key,
492             #pod and the previous value as the value. E.g. with this config:
493             #pod
494             #pod 'x-filter-output' => [
495             #pod [ 'yancy.wrap' => qw(user login) ],
496             #pod ],
497             #pod
498             #pod The original value of say C<{ user => 'bob', password => 'h12' }>
499             #pod will become:
500             #pod
501             #pod {
502             #pod login => {
503             #pod user => { user => 'bob', password => 'h12' }
504             #pod }
505             #pod }
506             #pod
507             #pod The utility of this comes from being able to expressively translate to
508             #pod and from a simple database structure to a situation where simple values
509             #pod or JSON objects need to be wrapped in objects one or two deep.
510             #pod
511             #pod =head4 yancy.unwrap
512             #pod
513             #pod This is the converse of the above. The configured parameters are a
514             #pod list of strings. For each one, the original value (a hash-ref) will be
515             #pod "unwrapped" by looking in the given hash and extracting the value whose
516             #pod key is that string. E.g. with this config:
517             #pod
518             #pod 'x-filter' => [
519             #pod [ 'yancy.unwrap' => qw(login user) ],
520             #pod ],
521             #pod
522             #pod This will achieve the reverse of the transformation given in
523             #pod L above. Note that obviously the order of arguments is
524             #pod inverted, since this operates outside-inward, while C
525             #pod operates inside-outward.
526             #pod
527             #pod =head4 yancy.mask
528             #pod
529             #pod Mask part of a field's value by replacing a regular expression match
530             #pod with the given character. The first parameter is a regular expression to
531             #pod match. The second parameter is the character to replace each matched
532             #pod character with.
533             #pod
534             #pod # Replace all text before the @ with *
535             #pod 'x-filter' => [
536             #pod [ 'yancy.mask' => '^[^@]+', '*' ]
537             #pod ],
538             #pod # Replace all but the last two characters before the @
539             #pod 'x-filter' => [
540             #pod [ 'yancy.mask' => '^[^@]+(?=[^@]{2}@)', '*' ]
541             #pod ],
542             #pod
543             #pod =head2 yancy.filter.apply
544             #pod
545             #pod B Filters are deprecated and will be removed in Yancy v2. See
546             #pod L for a way to replace them.
547             #pod
548             #pod my $filtered_data = $c->yancy->filter->apply( $schema, $item_data );
549             #pod
550             #pod Run the configured filters on the given C<$item_data>. C<$schema> is
551             #pod a schema name. Returns the hash of C<$filtered_data>.
552             #pod
553             #pod The property-level filters will run before any schema-level filter,
554             #pod so that schema-level filters can take advantage of any values set by
555             #pod the inner filters.
556             #pod
557             #pod =head2 yancy.filters
558             #pod
559             #pod B Filters are deprecated and will be removed in Yancy v2. See
560             #pod L for a way to replace them.
561             #pod
562             #pod Returns a hash-ref of all configured helpers, mapping the names to
563             #pod the code-refs.
564             #pod
565             #pod =head2 yancy.schema
566             #pod
567             #pod my $schema = $c->yancy->schema( $name );
568             #pod $c->yancy->schema( $name => $schema );
569             #pod my $schemas = $c->yancy->schema;
570             #pod
571             #pod Get or set the JSON schema for the given schema C<$name>. If no
572             #pod schema name is given, returns a hashref of all the schema.
573             #pod
574             #pod =head2 log_die
575             #pod
576             #pod Raise an exception with L, first logging
577             #pod using L (through the L helper|Mojolicious::Plugin::DefaultHelpers/log>.
578             #pod
579             #pod =head1 TEMPLATES
580             #pod
581             #pod This plugin uses the following templates. To override these templates
582             #pod with your own theme, provide a template with the same name. Remember to
583             #pod add your template paths to the beginning of the list of paths to be sure
584             #pod your templates are found first:
585             #pod
586             #pod # Mojolicious::Lite
587             #pod unshift @{ app->renderer->paths }, 'template/directory';
588             #pod unshift @{ app->renderer->classes }, __PACKAGE__;
589             #pod
590             #pod # Mojolicious
591             #pod sub startup {
592             #pod my ( $app ) = @_;
593             #pod unshift @{ $app->renderer->paths }, 'template/directory';
594             #pod unshift @{ $app->renderer->classes }, __PACKAGE__;
595             #pod }
596             #pod
597             #pod =over
598             #pod
599             #pod =item layouts/yancy.html.ep
600             #pod
601             #pod This layout template surrounds all other Yancy templates. Like all
602             #pod Mojolicious layout templates, a replacement should use the C
603             #pod helper to display the page content. Additionally, a replacement should
604             #pod use C<< content_for 'head' >> to add content to the C element.
605             #pod
606             #pod =back
607             #pod
608             #pod =head1 SEE ALSO
609             #pod
610             #pod =cut
611              
612 19     19   1555773 use Mojo::Base 'Mojolicious::Plugin';
  19         70  
  19         152  
613 19     19   12306 use Yancy;
  19         60  
  19         216  
614 19     19   816 use Mojo::JSON qw( true false decode_json );
  19         47  
  19         1148  
615 19     19   119 use Mojo::File qw( path );
  19         50  
  19         798  
616 19     19   117 use Mojo::Loader qw( load_class );
  19         39  
  19         868  
617 19     19   168 use Yancy::Util qw( load_backend curry copy_inline_refs derp is_type json_validator );
  19         54  
  19         1312  
618 19     19   10950 use Yancy::Model;
  19         63  
  19         164  
619 19     19   799 use Storable qw( dclone );
  19         49  
  19         1026  
620 19     19   126 use Scalar::Util qw( blessed );
  19         42  
  19         69066  
621              
622             has _filters => sub { {} };
623              
624             # NOTE: This class should largely be setup of paths and helpers. Special
625             # handling of route stashes should be in the controller object. Special
626             # handling of data should be in the model.
627             #
628             # Code in here is difficult to override for customizations, and should
629             # be avoided.
630              
631             sub register {
632 55     55 1 585380 my ( $self, $app, $config ) = @_;
633              
634             # XXX: Move editor, auth, schema to attributes of this object.
635             # That allows for easier extending/replacing of them.
636             # XXX: Deprecate direct access to the backend. Backend should be
637             # accessed through the schema, if needed.
638              
639             # New default for read_schema is on, since it mostly should be
640             # on. Any real-world database is going to be painstakingly tedious
641             # to type out in JSON schema...
642 55   100     483 $config->{read_schema} //= !exists $config->{openapi};
643              
644 55 50       234 if ( $config->{collections} ) {
645 0         0 derp '"collections" stash key is now "schema" in Yancy configuration';
646 0         0 $config->{schema} = $config->{collections};
647             }
648             die "Cannot pass both openapi AND (schema or read_schema)"
649             if $config->{openapi}
650 55 100 66     315 && ( $config->{schema} || $config->{read_schema} );
      66        
651              
652             # Load the backend and schema
653 54         310 $config = { %$config };
654             $app->helper( 'yancy.backend' => sub {
655 732     732   94143 my ( $c ) = @_;
656 732   66     1879 state $default_backend = load_backend( $config->{backend}, $config->{schema} || $config->{openapi}{definitions} );
657 732 50       2542 if ( my $backend = $c->stash( 'backend' ) ) {
658 0         0 $c->log->debug( 'Using override backend from stash: ' . ref $backend );
659 0         0 return $backend;
660             }
661 732         10873 return $default_backend;
662 54         616 } );
663              
664 54 100       19793 if ( $config->{openapi} ) {
665 4         24 $config->{openapi} = _ensure_json_data( $app, $config->{openapi} );
666 4         809 $config->{schema} = dclone( $config->{openapi}{definitions} );
667             }
668              
669 54 50 33     352 my $model = $config->{model} && blessed $config->{model} ? $config->{model} : undef;
670 54 50       245 if ( !$model ) {
671 54   50     365 my $class = $config->{model} // 'Yancy::Model';
672             $model = $class->new(
673             backend => $app->yancy->backend,
674             log => $app->log,
675             ( read_schema => $config->{read_schema} )x!!exists $config->{read_schema},
676             schema => $config->{schema},
677 54         362 );
678             }
679             $app->helper( 'yancy.model' => sub {
680 346     346   14924 my ( $c, $schema ) = @_;
681 346 100       1695 return $schema ? $model->schema( $schema ) : $model;
682 54         1234 } );
683              
684             # XXX: Add the fully-read schema back to the configuration hash.
685             # This will be removed in v2.
686 54 100       19106 my @schema_names = $config->{read_schema} ? $model->schema_names : grep { !$config->{schema}{$_}{'x-ignore'} } keys %{ $config->{schema} };
  28         97  
  8         43  
687 54         181 for my $schema_name ( @schema_names ) {
688 294         15800 my $schema = $model->schema( $schema_name );
689 292         931 $schema->_check_json_schema; # In case we haven't already
690 292         666 $config->{schema}{ $schema_name } = dclone( $schema->json_schema );
691             }
692             # XXX: Add the fully-read schema back to the backend. This should be
693             # removed in favor of the backend's read_schema filling things in.
694             # The backend should keep a copy of the original schema, as read
695             # from the database. The model's schema can be altered.
696 52         3141 $app->yancy->backend->schema( $model->json_schema );
697              
698             # Resources and templates
699 52         489 my $share = path( __FILE__ )->sibling( 'Yancy' )->child( 'resources' );
700 52         6851 push @{ $app->static->paths }, $share->child( 'public' )->to_string;
  52         282  
701 52         1672 push @{ $app->renderer->paths }, $share->child( 'templates' )->to_string;
  52         199  
702 52         1451 push @{$app->routes->namespaces}, 'Yancy::Controller';
  52         223  
703 52         515 push @{ $app->commands->namespaces }, 'Yancy::Command';
  52         228  
704 52         3530 $app->plugin( 'I18N', { namespace => 'Yancy::I18N' } );
705              
706             # Helpers
707 52     0   183560 $app->helper( 'yancy.config' => sub { return $config } );
  0         0  
708 52         19475 $app->helper( 'yancy.plugin' => \&_helper_plugin );
709 52         19533 $app->helper( 'yancy.schema' => \&_helper_schema );
710 52         20784 $app->helper( 'yancy.list' => \&_helper_list );
711 52         22443 $app->helper( 'yancy.get' => \&_helper_get );
712 52         23844 $app->helper( 'yancy.delete' => \&_helper_delete );
713 52         30860 $app->helper( 'yancy.set' => \&_helper_set );
714 52         27877 $app->helper( 'yancy.create' => \&_helper_create );
715 52         29448 $app->helper( 'yancy.validate' => \&_helper_validate );
716 52         31306 $app->helper( 'yancy.routify' => \&_helper_routify );
717 52         32366 $app->helper( 'log_die' => \&_helper_log_die );
718              
719             # Default form is Bootstrap4. Any form plugin added after this will
720             # override this one
721 52         3402 $app->yancy->plugin( 'Form::Bootstrap4' );
722 52         1749 $app->yancy->plugin( File => {
723             path => $app->home->child( 'public/uploads' ),
724             } );
725              
726             $self->_helper_filter_add( undef, 'yancy.from_helper' => sub {
727 1     1   7 my ( $field_name, $field_value, $field_conf, @params ) = @_;
728 1         3 my $which_helper = shift @params;
729 1         6 my $helper = $app->renderer->get_helper( $which_helper );
730 1         19 $helper->( @params );
731 52         65094 } );
732             $self->_helper_filter_add( undef, 'yancy.overlay_from_helper' => sub {
733 1     1   4 my ( $field_name, $field_value, $field_conf, @params ) = @_;
734 1         5 my %new_item = %$field_value;
735 1         6 while ( my ( $key, $helper ) = splice @params, 0, 2 ) {
736 1 50       5 ( $helper, my @this_params ) = @$helper if ref $helper eq 'ARRAY';
737 1         6 my $v = $app->renderer->get_helper( $helper )->( @this_params );
738 1         23 $new_item{ $key } = $v;
739             }
740 1         5 \%new_item;
741 52         387 } );
742             $self->_helper_filter_add( undef, 'yancy.wrap' => sub {
743 3     3   11 my ( $field_name, $field_value, $field_conf, @params ) = @_;
744 3         14 $field_value = { $_ => $field_value } for @params;
745 3         14 $field_value;
746 52         498 } );
747             $self->_helper_filter_add( undef, 'yancy.unwrap' => sub {
748 1     1   4 my ( $field_name, $field_value, $field_conf, @params ) = @_;
749 1         6 $field_value = $field_value->{$_} for @params;
750 1         6 $field_value;
751 52         467 } );
752             $self->_helper_filter_add( undef, 'yancy.mask' => sub {
753 3     3   25 my ( $field_name, $field_value, $field_conf, $regex, $replace ) = @_;
754 3         113 $field_value =~ s/($regex)/$replace x length $1/e;
  3         33  
755 3         28 $field_value;
756 52         476 } );
757 52         262 for my $name ( keys %{ $config->{filters} } ) {
  52         283  
758 1         4 $self->_helper_filter_add( undef, $name, $config->{filters}{$name} );
759             }
760 52         296 $app->helper( 'yancy.filter.add' => curry( \&_helper_filter_add, $self ) );
761 52         80770 $app->helper( 'yancy.filter.apply' => curry( \&_helper_filter_apply, $self ) );
762             $app->helper( 'yancy.filters' => sub {
763 1     1   1508 state $filters = $self->_filters;
764 52         81486 } );
765              
766             # Some keys we used to allow on the top level configuration, but are
767             # now on the editor plugin
768 52         82949 my @_moved_to_editor_keys = qw( api_controller info host return_to );
769 52 50       469 if ( my @moved_keys = grep exists $config->{$_}, @_moved_to_editor_keys ) {
770 0         0 derp 'Editor configuration keys should be in the `editor` configuration hash ref: '
771             . join ', ', @moved_keys;
772             }
773              
774             # Add the default editor unless the user explicitly disables it
775 52 50 66     420 if ( !exists $config->{editor} || defined $config->{editor} ) {
776             $app->yancy->plugin( 'Editor' => {
777             (
778 115         336 map { $_ => $config->{ $_ } }
779 416         7177 grep { defined $config->{ $_ } }
780             qw( openapi schema route read_schema ),
781             @_moved_to_editor_keys,
782             ),
783 52   100     264 %{ $config->{editor} // {} },
  52         560  
784             } );
785             }
786             }
787              
788             # if false or a ref, just returns same
789             # if non-ref, treat as JSON-containing file, load and decode
790             sub _ensure_json_data {
791 4     4   13 my ( $app, $data ) = @_;
792 4 50 33     36 return $data if !$data or ref $data;
793             # assume a file in JSON format: load and parse it
794 0         0 decode_json $app->home->child( $data )->slurp;
795             }
796              
797             sub _helper_plugin {
798 182     182   36612 my ( $c, $name, @args ) = @_;
799 182         571 my $class = 'Yancy::Plugin::' . $name;
800 182 100       899 if ( my $e = load_class( $class ) ) {
801 1 50       637 die ref $e ? "Could not load class $class: $e" : "Could not find class $class";
802             }
803 181         7699 my $plugin = $class->new;
804 181         3643 $plugin->register( $c->app, @args );
805             }
806              
807             sub _helper_schema {
808 481     481   48747 my ( $c, $name, $schema ) = @_;
809             # XXX: This helper must be deprecated in favor of using the Model,
810             # because it'd just be better to have a smaller API.
811 481 100       1491 if ( !$name ) {
812 2         13 return $c->yancy->backend->schema;
813             }
814 479 100       1322 if ( $schema ) {
815 14         94 $c->yancy->backend->schema->{ $name } = $schema;
816 14         64 return;
817             }
818 465   100     1502 my $info = copy_inline_refs( $c->yancy->backend->schema, "/$name" ) || return undef;
819 461 50       3200 return keys %$info ? $info : undef;
820             }
821              
822             sub _helper_list {
823 10     10   6455 my ( $c, $schema_name, @args ) = @_;
824 10         26 my @items = @{ $c->yancy->model( $schema_name )->list( @args )->{items} };
  10         51  
825 10         262 my $schema = $c->yancy->schema( $schema_name );
826 10         29 for my $prop_name ( keys %{ $schema->{properties} } ) {
  10         62  
827 58         106 my $prop = $schema->{properties}{ $prop_name };
828 58 100 100     191 if ( $prop->{format} && $prop->{format} eq 'password' ) {
829 4         113 delete $_->{ $prop_name } for @items;
830             }
831             }
832 10         40 return map { $c->yancy->filter->apply( $schema_name, $_, 'x-filter-output' ) } @items;
  10         47  
833             }
834              
835             sub _helper_get {
836 11     11   35784 my ( $c, $schema_name, $id, @args ) = @_;
837 11         69 my $item = $c->yancy->model( $schema_name )->get( $id, @args );
838 11         243 my $schema = $c->yancy->schema( $schema_name );
839 11         32 for my $prop_name ( keys %{ $schema->{properties} } ) {
  11         64  
840 79         303 my $prop = $schema->{properties}{ $prop_name };
841 79 100 100     271 if ( $prop->{format} && $prop->{format} eq 'password' ) {
842 2         13 delete $item->{ $prop_name };
843             }
844             }
845 11         65 $item = $c->yancy->filter->apply( $schema_name, $item, 'x-filter-output' );
846 11         142 return $item;
847             }
848              
849             sub _helper_delete {
850 1     1   7455 my ( $c, $schema_name, @args ) = @_;
851 1         20 return $c->yancy->model( $schema_name )->delete( @args );
852             }
853              
854             sub _helper_set {
855 11     11   24487 my ( $c, $schema, $id, $item, %opt ) = @_;
856 11         84 $item = $c->yancy->filter->apply( $schema, $item );
857 11         56 return $c->yancy->model( $schema )->set( $id, $item );
858             }
859              
860             sub _helper_create {
861 19     19   27259 my ( $c, $schema, $item ) = @_;
862              
863 19         100 my $props = $c->yancy->schema( $schema )->{properties};
864             # XXX: We need to fix the way defaults get set: Defaults that are
865             # set by the database must not be set here. See Github #124
866             $item->{ $_ } = $props->{ $_ }{default}
867 19   100     489 for grep !exists $item->{ $_ } && exists $props->{ $_ }{default},
868             keys %$props;
869              
870 19         217 $item = $c->yancy->filter->apply( $schema, $item );
871 19         135 return $c->yancy->model( $schema )->create( $item );
872             }
873              
874             sub _helper_validate {
875 0     0   0 my ( $c, $schema_name, $input_item, %opt ) = @_;
876 0         0 return $c->yancy->model( $schema_name )->validate( $input_item, %opt );
877             }
878              
879             sub _helper_filter_apply {
880 266     266   889 my ( $self, $c, $schema_name, $item, $output ) = @_;
881 266 100       852 my $filter_type = $output ? 'x-filter-output' : 'x-filter';
882 266         767 my $schema = $c->yancy->schema( $schema_name );
883 266         1331 my $filters = $self->_filters;
884 266         1676 for my $key ( keys %{ $schema->{properties} } ) {
  266         1368  
885 1942 100       4968 next unless my $prop_filters = $schema->{properties}{ $key }{ $filter_type };
886 28         74 for my $filter ( @{ $prop_filters } ) {
  28         92  
887 29 100       111 ( $filter, my @params ) = @$filter if ref $filter eq 'ARRAY';
888 29         93 my $sub = $filters->{ $filter };
889 29 100       119 $c->log_die( "Unknown filter: $filter (schema: $schema_name, field: $key)" )
890             unless $sub;
891             $item = { %$item, $key => $sub->(
892 28         189 $key, $item->{ $key }, $schema->{properties}{ $key }, @params
893             ) };
894             }
895             }
896 265 100       1140 if ( my $schema_filters = $schema->{$filter_type} ) {
897 3         6 for my $filter ( @{ $schema_filters } ) {
  3         10  
898 2 100       11 ( $filter, my @params ) = @$filter if ref $filter eq 'ARRAY';
899 2         5 my $sub = $filters->{ $filter };
900 2 50       6 $c->log_die( "Unknown filter: $filter (schema: $schema_name)" )
901             unless $sub;
902 2         9 $item = $sub->( $schema_name, $item, $schema, @params );
903             }
904             }
905 265         2908 return $item;
906             }
907              
908             sub _helper_filter_add {
909 279     279   656 my ( $self, $c, $name, $sub ) = @_;
910 279         806 $self->_filters->{ $name } = $sub;
911             }
912              
913             sub _helper_routify {
914 82     82   11140 my ( $self, @args ) = @_;
915 82         678 for my $maybe_route ( @args ) {
916 155 100       503 next unless defined $maybe_route;
917 75 100 66     702 return blessed $maybe_route && $maybe_route->isa( 'Mojolicious::Routes::Route' )
918             ? $maybe_route
919             : $self->app->routes->any( $maybe_route )
920             ;
921             }
922             }
923              
924             sub _helper_log_die {
925 1     1   26 my ( $self, $class, $err ) = @_;
926             # XXX: Handle JSON::Validator errors
927 1 50       6 if ( !$err ) {
928 1         5 $err = $class;
929 1         2 $class = 'Mojo::Exception';
930             }
931 1 50       17 if ( !$class->can( 'new' ) ) {
932 0 0       0 die $@ unless eval "package $class; use Mojo::Base 'Mojo::Exception'; 1";
933             }
934 1         8 my $e = $class->new( $err )->trace( 2 );
935 1         172 $self->log->fatal( $e );
936 1         662 die $e;
937             }
938              
939             1;
940              
941             __END__