From c50e4ec3c085d4a94e4112c7bc7c27f81e44aef8 Mon Sep 17 00:00:00 2001 From: Doug Bell Date: Fri, 7 Aug 2020 18:53:33 -0500 Subject: [PATCH] make yancy easier to set up for basic admin-ing Now Yancy can be easily configured with just a database object to provide a useful admin panel. This changes the default for read_schema to on, since most of the time you want it on anyway. --- lib/Mojolicious/Plugin/Yancy.pm | 9 +- lib/Yancy.pm | 289 ++++++++++++++++++++++---------- lib/Yancy/Help/Config.pod | 4 +- lib/Yancy/Plugin/Editor.pm | 10 +- lib/Yancy/Util.pm | 28 ++++ t/config.t | 7 + t/examples/doc-site.t | 2 +- t/register.t | 68 ++++++++ 8 files changed, 316 insertions(+), 101 deletions(-) create mode 100644 t/register.t diff --git a/lib/Mojolicious/Plugin/Yancy.pm b/lib/Mojolicious/Plugin/Yancy.pm index 6cb93a1f..1e4cd4af 100644 --- a/lib/Mojolicious/Plugin/Yancy.pm +++ b/lib/Mojolicious/Plugin/Yancy.pm @@ -608,6 +608,11 @@ has _filters => sub { {} }; sub register { my ( $self, $app, $config ) = @_; + # New default for read_schema is on, since it mostly should be + # on. Any real-world database is going to be painstakingly tedious + # to type out in JSON schema... + $config->{read_schema} //= !exists $config->{openapi}; + if ( $config->{collections} ) { derp '"collections" stash key is now "schema" in Yancy configuration'; $config->{schema} = $config->{collections}; @@ -745,7 +750,7 @@ sub register { # Some keys we used to allow on the top level configuration, but are # now on the editor plugin - my @_moved_to_editor_keys = qw( route api_controller info host return_to ); + my @_moved_to_editor_keys = qw( api_controller info host return_to ); if ( my @moved_keys = grep exists $config->{$_}, @_moved_to_editor_keys ) { derp 'Editor configuration keys should be in the `editor` configuration hash ref: ' . join ', ', @moved_keys; @@ -757,7 +762,7 @@ sub register { ( map { $_ => $config->{ $_ } } grep { defined $config->{ $_ } } - qw( openapi schema ), + qw( openapi schema route ), @_moved_to_editor_keys, ), %{ $config->{editor} // {} }, diff --git a/lib/Yancy.pm b/lib/Yancy.pm index 4db11ef3..bf55fa02 100644 --- a/lib/Yancy.pm +++ b/lib/Yancy.pm @@ -6,12 +6,22 @@ our $VERSION = '1.066'; =head1 SYNOPSIS - use Mojolicious::Lite; - use Mojo::Pg; # Supported backends: Mojo::Pg, Mojo::mysql, Mojo::SQLite, DBIx::Class - plugin Yancy => { - backend => { Pg => Mojo::Pg->new( 'postgres:///myapp' ) }, - read_schema => 1, - }; + # Mojolicious + $self->plugin( Yancy => backend => 'postgresql://postgres@/mydb' ); + + # Mojolicious::Lite + plugin Yancy => backend => 'postgresql://postgres@/mydb'; # mysql, sqlite, dbic... + + # Secure access to the admin UI with Basic authentication + my $under = $app->routes->under( '/yancy', sub( $c ) { + return 1 if $c->req->url->to_abs->userinfo eq 'Bender:rocks'; + $c->res->headers->www_authenticate('Basic'); + $c->render(text => 'Authentication required!', status => 401); + return undef; + }); + $self->plugin( Yancy => backend => 'postgresql://postgres@/mydb', route => $under ); + + # ... then load the editor at http://127.0.0.1:3000/yancy =head1 DESCRIPTION @@ -28,142 +38,237 @@ our $VERSION = '1.066'; =end html -L is a simple content management system (CMS) for administering -content in a database. Yancy accepts a configuration file that describes -the data in the database, builds a website that lists all of the -available data, and allows a user to edit data, delete data, and add new -data. +L is a content management system (CMS) for L. It +includes an admin application to edit content and tools to quickly build +an application. -Yancy uses L to describe the data in -a database and configure forms and applications. The schema is added to -an L which creates a L for -your data. +=head2 Admin App -Yancy can be run in a standalone mode (which can be placed behind -a proxy), or can be embedded as a plugin into any application that uses -the L web framework. +Yancy provides an application to edit content at the path C on +your website. Yancy can manage data in multiple databases using +different L. You can provide a URL +string to tell Yancy how to connect to your database, or you can provide +your database object. Yancy supports the following databases: -Yancy can manage data in multiple databases using different backends -(L modules). Backends exist for L, L, L, L, and even L. +=head3 Postgres -=head2 Mojolicious Plugin +L is supported through the L +module. -Yancy is primarily a Mojolicious plugin to ease development and -management of Mojolicious applications. Yancy provides: + # PostgreSQL: A Mojo::Pg connection string + plugin Yancy => backend => 'postgresql://postgres@/test'; -=over + # PostgreSQL: A Mojo::Pg object + plugin Yancy => backend => Mojo::Pg->new( 'postgresql://postgres@/test' ); -=item * +=head3 MySQL -An L that allows -editing and managing your site's content. This app is highly -customizable using additional JavaScript. +L is supported through the L +module. -=item * + # MySQL: A Mojo::mysql connection string + plugin Yancy => backend => 'mysql://user@/test'; -L which you can use to easily -L, L, L, and L. + # MySQL: A Mojo::mysql object + plugin Yancy => backend => Mojo::mysql->strict_mode( 'mysql://user@/test' ); -=item * +=head3 SQLite -L to access data, validate -forms +L is supported through the L module. +This is a good option if you want to try Yancy out. -=item * + # SQLite: A Mojo::SQLite connection string + plugin Yancy => backend => 'sqlite:test.db'; -L which you can override -to customize the Yancy editor's appearance + # SQLite: A Mojo::SQLite object + plugin Yancy => backend => Mojo::SQLite->new( 'sqlite::temp:' ); -=back +=head3 DBIx::Class -For information on how to use Yancy as a Mojolicious plugin, see -L. +If you have a L schema, Yancy can use it to edit the content. -=head2 Example Applications + # DBIx::Class: A connection string + plugin Yancy => backend => 'dbic://My::Schema/dbi:SQLite:test.db'; -The L -includes some example applications you can use to help build your own -websites. L. + # DBIx::Class: A DBIx::Class::Schema object + plugin Yancy => backend => My::Schema->connect( 'dbi:SQLite:test.db' ); -=head2 Yancy Plugins +=head2 Content Tools -Yancy comes with plugins to enhance your website. +=head3 Schema Information and Validation -=over +Yancy scans your database to determine what kind of data is inside, but +Yancy also accepts a L to add more +information about your data. You can add descriptions, examples, and +other documentation that will appear in the admin application. You can +also add type, format, and other validation information, which Yancy +will use to validate input from users. See L +for how to define your schema. -=item * + plugin Yancy => backend => 'postgres://postgres@/test', + schema => { + employees => { + title => 'Employees', + description => 'Our crack team of loyal dregs.', + properties => { + address => { + description => 'Where to notify next-of-kin.', + # Regexp to validate this field + pattern => '^\d+ \S+', + }, + email => { + # Use the browser's native e-mail input + format => 'email', + }, + }, + }, + }; -L allows for customization of -the Yancy editor application, including adding your own components and -editors. +=head3 Data Helpers -=item * +L provides helpers to work with your database content. +These use the validations provided in the schema to validate user input. These +helpers can be used in your route handlers to quickly add basic Create, Read, Update, +and Delete (CRUD) functionality. See L for a list +of provided helpers. -L manages files uploaded to the -site via the L or via -a L. + # View a list of blog entries + get '/' => sub( $c ) { + my @blog_entries = $c->yancy->list( + blog_entries => + { published => 1 }, + { order_by => { -desc => 'published_date' } }, + ); + $c->render( + 'blog_list', + items => \@blog_entries, + ); + }; -=item * + # View a single blog entry + get '/blog/:blog_entry_id' => sub( $c ) { + my $blog_entry = $c->yancy->get( + blog_entries => $c->param( 'blog_entry_id' ), + ); + $c->render( + 'blog_entry', + item => $blog_entry, + ); + }; -L provides an API that allows you -to enable multiple authentication mechanisms for your site, including -L, or -users using their L or other -L. +=head3 Forms + +The L plugin can generate input fields or entire +forms based on your schema information. The annotations in your schema +appear in the forms to help users fill them out. Additionally, with the +L module, Yancy can create forms using +L components. + + # Load the form plugin + app->yancy->plugin( 'Form::Bootstrap4' ); + + # Edit a blog entry + any [ 'GET', 'POST' ], '/edit/:blog_entry_id' => sub( $c ) { + if ( $c->req->method eq 'GET' ) { + my $blog_entry = $c->yancy->get( + blog_entries => $c->param( 'blog_entry_id' ), + ); + return $c->render( + 'blog_entry', + item => $blog_entry, + ); + } + my $id = $c->param( 'blog_entry_id' ); + my $item = $c->req->params->to_hash; + delete $item->{csrf_token}; # See https://docs.mojolicious.org/Mojolicious/Guides/Rendering#Cross-site-request-forgery + $c->yancy->set( blog_entries => $id, $c->req->params->to_hash ); + $c->redirect_to( '/blog/' . $id ); + }; -=item * + __DATA__ + @@ blog_form.html.ep + %= $c->yancy->form->form_for( 'blog_entries', item => stash 'item' ) -L can generate forms for the -configured schema, or for individual fields. There are included -form generators for L. +=head3 Controllers -=back +Yancy can add basic CRUD operations without writing the code yourself. The +L module uses the schema information to show, search, +edit, create, and delete database items. -More development will be happening here soon! + # A rewrite of the routes above to use Yancy::Controller::Yancy + + # View a list of blog entries + get '/' => { + controller => 'yancy', + action => 'list', + schema => 'blog_entries', + filter => { published => 1 }, + order_by => { -desc => 'published_date' }, + } => 'blog.list'; -=head2 Standalone App + # View a single blog entry + get '/blog/:blog_entry_id' => { + controller => 'yancy', + action => 'get', + schema => 'blog_entries', + } => 'blog.get'; -Yancy can also be run as a standalone app in the event one wants to -develop applications solely using Mojolicious templates. For -information on how to run Yancy as a standalone application, see -L. + # Load the form plugin + app->yancy->plugin( 'Form::Bootstrap4' ); -=head2 REST API + # Edit a blog entry + any [ 'GET', 'POST' ], '/edit/:blog_entry_id' => { + controller => 'yancy', + action => 'set', + schema => 'blog_entries', + template => 'blog_form', + redirect_to => 'blog.get', + } => 'blog.edit'; -This application creates a REST API using the standard -L API specification. The API spec document -is located at C. + __DATA__ + @@ blog_form.html.ep + %= $c->yancy->form->form_for( 'blog_entries' ) -=head2 Internationalization +=head3 Plugins -Yancy is working on support for translating all internal text. See L -for more information. Translations and other help will be greatly appreciated! +Yancy also has plugins for... + +=over + +=item * User authentication: L + +=item * File management: L + +=back + +More development will be happening here soon! =head1 GUIDES +For in-depth documentation on Yancy, see the following guides: + =over =item * L - How to configure Yancy +=item * L - How to cook various apps with Yancy + =item * L - How to authenticate and authorize users -=item * L - How to use Yancy without writing code +=item * L - How to use Yancy without a Mojolicious app =item * L - How to upgrade from previous versions -=item * L - How to cook various apps with Yancy - =back +=head1 OTHER RESOURCES + +=head2 Example Applications + +The L +includes some example applications you can use to help build your own +websites. L. + =head1 BUNDLED PROJECTS This project bundles some other projects with the following licenses: @@ -189,7 +294,7 @@ be sure to watch the changelog for version updates. =head1 SEE ALSO -L, L +L =cut diff --git a/lib/Yancy/Help/Config.pod b/lib/Yancy/Help/Config.pod index 6c0b40f9..ec6c124e 100644 --- a/lib/Yancy/Help/Config.pod +++ b/lib/Yancy/Help/Config.pod @@ -94,7 +94,7 @@ below. See your backend's documentation for more information. =back -=head1 Data Collections +=head1 Schema The C data structure defines what data is in the database. Each key in this structure refers to the name of a schema, and the @@ -106,7 +106,7 @@ are columns. For an ORM like DBIx::Class, the schemas are ResultSet objects. For a document store like MongoDB, the schemas are collections. See your backend's documentation for more information. -Collections are configured using L. +Schemas are configured using L. The JSON Schema defines what fields (properties) an item has, and what type of data those field have. The JSON Schema also can define constraints like required fields or validate strings with regular diff --git a/lib/Yancy/Plugin/Editor.pm b/lib/Yancy/Plugin/Editor.pm index 115e08fd..4a12ab54 100644 --- a/lib/Yancy/Plugin/Editor.pm +++ b/lib/Yancy/Plugin/Editor.pm @@ -228,10 +228,12 @@ sub register { state $auth_cb = $c->yancy->can( 'auth' ) && $c->yancy->auth->can( 'require_user' ) && $c->yancy->auth->require_user( $config->{require_user} || () ); - if ( !$auth_cb && !exists $config->{require_user} ) { - derp qq{*** Editor without authentication is deprecated and will be\n} - . qq{removed in v2.0. Configure an Auth plugin or set \n} - . qq{`editor.require_user => undef` to silence this warning\n}; + if ( !$auth_cb && !exists $config->{require_user} && !defined $config->{route} ) { + $app->log->warn( + qq{*** Cannot verify that admin editor is behind authentication.\n} + . qq{Add a Yancy Auth plugin, add a `route` to the Yancy plugin config,\n} + . qq{or set `editor.require_user => undef` to silence this warning\n} + ); } return $auth_cb ? $auth_cb->( $c ) : 1; }; diff --git a/lib/Yancy/Util.pm b/lib/Yancy/Util.pm index 0bd8391b..1e8c51f5 100644 --- a/lib/Yancy/Util.pm +++ b/lib/Yancy/Util.pm @@ -48,6 +48,7 @@ our @EXPORT_OK = qw( load_backend curry currym copy_inline_refs match derp fill_ my $backend = load_backend( $backend_url, $schema ); my $backend = load_backend( { $backend_name => $arg }, $schema ); + my $backend = load_backend( $db_object, $schema ); Get a Yancy backend from the given backend URL, or from a hash reference with a backend name and optional argument. The C<$schema> hash is @@ -61,19 +62,46 @@ The C<$backend_name> should be the name of a module in the C namespace. The C<$arg> is handled by the backend module. Read your backend module's documentation for details. +The C<$db_object> can be one of: L, L, +L, or a subclass of L. The +appropriate backend object will be created. + See L for information about backend URLs and L for more information about backend objects. =cut +# This allows users to pass in the database object directly +our %BACKEND_CLASSES = ( + 'Mojo::Pg' => 'pg', + 'Mojo::mysql' => 'mysql', + 'Mojo::SQLite' => 'sqlite', + 'DBIx::Class::Schema' => 'dbic', +); + +# Aliases allow the user to specify the same string as they pass to +# their database object +our %TYPE_ALIAS = ( + postgresql => 'pg', +); + sub load_backend { my ( $config, $schema ) = @_; my ( $type, $arg ); if ( !ref $config ) { ( $type ) = $config =~ m{^([^:]+)}; + $type = $TYPE_ALIAS{ $type } // $type; $arg = $config; } + elsif ( blessed $config ) { + for my $class ( keys %BACKEND_CLASSES ) { + if ( $config->isa( $class ) ) { + ( $type, $arg ) = ( $BACKEND_CLASSES{ $class }, $config ); + last; + } + } + } else { ( $type, $arg ) = %{ $config }; } diff --git a/t/config.t b/t/config.t index ec2f761b..85be4578 100644 --- a/t/config.t +++ b/t/config.t @@ -141,6 +141,7 @@ subtest 'read_schema' => sub { people => { read_schema => 1 }, }, editor => { require_user => undef, }, + read_schema => 0, }, ); @@ -203,6 +204,7 @@ subtest 'errors' => sub { subtest 'missing id field' => sub { my %missing_id = ( + backend => $backend_url, schema => { foo => { type => 'object', @@ -212,6 +214,7 @@ subtest 'errors' => sub { }, }, editor => { require_user => undef, }, + read_schema => 0, ); eval { Yancy->new( config => \%missing_id ) }; ok $@, 'configuration dies'; @@ -219,6 +222,7 @@ subtest 'errors' => sub { 'error is correct'; my %missing_x_id = ( + backend => $backend_url, schema => { foo => { type => 'object', @@ -229,6 +233,7 @@ subtest 'errors' => sub { }, }, editor => { require_user => undef, }, + read_schema => 0, ); eval { Yancy->new( config => \%missing_x_id ) }; ok $@, 'configuration dies'; @@ -236,6 +241,7 @@ subtest 'errors' => sub { 'error is correct'; my %ignored_missing_id = ( + backend => $backend_url, schema => { foo => { 'x-ignore' => 1, @@ -245,6 +251,7 @@ subtest 'errors' => sub { }, }, editor => { require_user => undef, }, + read_schema => 0, ); eval { Yancy->new( config => \%ignored_missing_id ) }; ok !$@, 'configuration succeeds' or diag $@; diff --git a/t/examples/doc-site.t b/t/examples/doc-site.t index 2ad4a433..4e8c6afa 100644 --- a/t/examples/doc-site.t +++ b/t/examples/doc-site.t @@ -45,7 +45,7 @@ subtest 'test pages' => sub { subtest 'test pod viewer' => sub { $t->get_ok( '/perldoc' )->status_is( 200 ) ->element_exists( 'h1#SEE-ALSO', 'doc section SEE ALSO exists' ) - ->element_exists( 'h2#Mojolicious-Plugin', 'doc section Mojolicious Plugin exists' ); + ->element_exists( 'h2#Admin-App', 'doc section Admin App exists' ); }; subtest 'test export' => sub { diff --git a/t/register.t b/t/register.t new file mode 100644 index 00000000..0e58ad93 --- /dev/null +++ b/t/register.t @@ -0,0 +1,68 @@ + +=head1 DESCRIPTION + +This tests registering the Mojolicious::Plugin::Yancy and the various kinds +of arguments that can be given. + +=cut + +use Mojo::Base -strict; +use Test::More; +use Test::Mojo; +use Yancy; +use FindBin qw( $Bin ); +use Mojo::File qw( path ); +use lib "".path( $Bin, 'lib' ); +use Local::Test qw( init_backend ); + +my ( $backend_url, $backend, %items ) = init_backend( {} ); + +subtest 'sqlite' => sub { + if ( !eval { require Yancy::Backend::Sqlite; 1 } ) { + diag $@; + pass 'skipped: Mojo::SQLite required'; + return; + } + + subtest 'bare sqlite database (Mojo::SQLite)' => sub { + my $app = Mojolicious->new; + my $db = Mojo::SQLite->new( 'sqlite::memory:' ); + eval { $app->plugin( Yancy => backend => $db ) }; + ok !$@, 'plugin is loaded' or diag $@; + isa_ok $app->yancy->backend, 'Yancy::Backend::Sqlite', 'backend object is correct'; + }; +}; + +subtest 'postgresql' => sub { + if ( !eval { require Yancy::Backend::Pg; 1 } ) { + diag $@; + pass 'skipped: Mojo::Pg required'; + return; + } + + subtest 'postgresql database (Mojo::Pg)' => sub { + my $app = Mojolicious->new; + my $db = bless {}, 'Mojo::Pg'; + eval { $app->plugin( Yancy => backend => $db, read_schema => 0 ) }; + ok !$@, 'plugin is loaded' or diag $@; + isa_ok $app->yancy->backend, 'Yancy::Backend::Pg', 'backend object is correct'; + }; +}; + +subtest 'mysql' => sub { + if ( !eval { require Yancy::Backend::Mysql; 1 } ) { + diag $@; + pass 'skipped: Mojo::mysql required'; + return; + } + + subtest 'postgresql database (Mojo::Mysql)' => sub { + my $app = Mojolicious->new; + my $db = Mojo::mysql->new(); + eval { $app->plugin( Yancy => backend => $db, read_schema => 0 ) }; + ok !$@, 'plugin is loaded' or diag $@; + isa_ok $app->yancy->backend, 'Yancy::Backend::Mysql', 'backend object is correct'; + }; +}; + +done_testing;