NAME
Mojolicious::Plugin::Yancy - Embed a simple admin CMS into your Mojolicious application
VERSION
version 1.078
SYNOPSIS
use Mojolicious::Lite;
plugin Yancy => backend => 'sqlite:myapp.db'; # mysql, pg, dbic...
app->start;
DESCRIPTION
This plugin allows you to add a simple content management system (CMS) to administrate content on your Mojolicious site. This includes a JavaScript web application to edit the content and a REST API to help quickly build your own application.
CONFIGURATION
For getting started with a configuration for Yancy, see the "Yancy Guides".
Additional configuration keys accepted by the plugin are:
- backend
-
In addition to specifying the backend as a single URL (see "Database Backend"), you can specify it as a hashref of
class => $db
. This allows you to share database connections.use Mojolicious::Lite; use Mojo::Pg; helper pg => sub { state $pg = Mojo::Pg->new( 'postgres:///myapp' ) }; plugin Yancy => { backend => { Pg => app->pg } };
- route
-
A base route to add the Yancy editor to. This allows you to customize the URL and add authentication or authorization. Defaults to allowing access to the Yancy web application under
/yancy
, and the REST API under/yancy/api
.This can be a string or a Mojolicious::Routes::Route object.
# These are equivalent use Mojolicious::Lite; plugin Yancy => { route => app->routes->any( '/admin' ) }; plugin Yancy => { route => '/admin' };
- return_to
-
The URL to use for the "Back to Application" link. Defaults to
/
. - filters
-
A hash of
name => subref
pairs of filters to make available. See "yancy.filter.add" for how to create a filter subroutine.
HELPERS
This plugin adds some helpers for use in routes, templates, and plugins.
yancy.config
my $config = $c->yancy->config;
The current configuration for Yancy. Through this, you can edit the schema
configuration as needed.
yancy.backend
my $be = $c->yancy->backend;
Get the Yancy backend object. By default, gets the backend configured while loading the Yancy plugin. Requests can override the backend by setting the backend
stash value. See Yancy::Backend for the methods you can call on a backend object and their purpose.
yancy.plugin
Add a Yancy plugin. Yancy plugins are Mojolicious plugins that require Yancy features and are found in the Yancy::Plugin namespace.
use Mojolicious::Lite;
plugin 'Yancy';
app->yancy->plugin( 'Auth::Basic', { schema => 'users' } );
You can also add the Yancy::Plugin namespace into the default plugin lookup locations. This allows you to treat them like any other Mojolicious plugin.
# Lite app
use Mojolicious::Lite;
plugin 'Yancy', ...;
unshift @{ app->plugins->namespaces }, 'Yancy::Plugin';
plugin 'Auth::Basic', ...;
# Full app
use Mojolicious;
sub startup {
my ( $app ) = @_;
$app->plugin( 'Yancy', ... );
unshift @{ $app->plugins->namespaces }, 'Yancy::Plugin';
$app->plugin( 'Auth::Basic', ... );
}
Yancy does not do this for you to avoid namespace collisions.
yancy.list
my @items = $c->yancy->list( $schema, \%param, \%opt );
Get a list of items from the backend. $schema
is a schema name. \%param
is a SQL::Abstract where clause structure. Some basic examples:
# All people named exactly 'Turanga Leela'
$c->yancy->list( people => { name => 'Turanga Leela' } );
# All people with "Wong" in their name
$c->yancy->list( people => { name => { like => '%Wong%' } } );
\%opt
is a hash of options with the following keys:
limit - The number of items to return
offset - The number of items to skip before returning items
See the backend documentation for more information about the list method's arguments. This helper only returns the list of items, not the total count of items or any other value.
This helper will also filter out any password fields in the returned data. To get all the data, use the backend helper to access the backend methods directly.
yancy.get
my $item = $c->yancy->get( $schema, $id );
Get an item from the backend. $schema
is the schema name. $id
is the ID of the item to get. See "get" in Yancy::Backend.
This helper will filter out password values in the returned data. To get all the data, use the backend helper to access the backend directly.
yancy.set
$c->yancy->set( $schema, $id, $item_data, %opt );
Update an item in the backend. $schema
is the schema name. $id
is the ID of the item to update. $item_data
is a hash of data to update. See "set" in Yancy::Backend. %opt
is a list of options with the following keys:
properties - An arrayref of properties to validate, for partial updates
This helper will validate the data against the configuration and run any filters as needed. If validation fails, this helper will throw an exception with an array reference of JSON::Validator::Error objects. See the validate helper and the filter apply helper. To bypass filters and validation, use the backend object directly via the backend helper.
# A route to update a comment
put '/comment/:id' => sub {
eval { $c->yancy->set( "comment", $c->stash( 'id' ), $c->req->json ) };
if ( $@ ) {
return $c->render( status => 400, errors => $@ );
}
return $c->render( status => 200, text => 'Success!' );
};
yancy.create
my $item = $c->yancy->create( $schema, $item_data );
Create a new item. $schema
is the schema name. $item_data
is a hash of data for the new item. See "create" in Yancy::Backend.
This helper will validate the data against the configuration and run any filters as needed. If validation fails, this helper will throw an exception with an array reference of JSON::Validator::Error objects. See the validate helper and the filter apply helper. To bypass filters and validation, use the backend object directly via the backend helper.
# A route to create a comment
post '/comment' => sub {
eval { $c->yancy->create( "comment", $c->req->json ) };
if ( $@ ) {
return $c->render( status => 400, errors => $@ );
}
return $c->render( status => 200, text => 'Success!' );
};
yancy.delete
$c->yancy->delete( $schema, $id );
Delete an item from the backend. $schema
is the schema name. $id
is the ID of the item to delete. See "delete" in Yancy::Backend.
yancy.validate
my @errors = $c->yancy->validate( $schema, $item, %opt );
Validate the given $item
data against the configuration for the $schema
. If there are any errors, they are returned as an array of JSON::Validator::Error objects. %opt
is a list of options with the following keys:
properties - An arrayref of properties to validate, for partial updates
See "validate" in JSON::Validator for more details.
yancy.form
By default, the Yancy::Plugin::Form::Bootstrap4 form plugin is loaded. You can override this with your own form plugin. See Yancy::Plugin::Form for more information.
yancy.file
By default, the Yancy::Plugin::File plugin is loaded to handle file uploading and file management. The default path for file uploads is $MOJO_HOME/public/uploads
. You can override this with your own file plugin. See Yancy::Plugin::File for more information.
yancy.filter.add
my $filter_sub = sub { my ( $field_name, $field_value, $field_conf, @params ) = @_; ... }
$c->yancy->filter->add( $name => $filter_sub );
Create a new filter. $name
is the name of the filter to give in the field's configuration. $subref
is a subroutine reference that accepts at least three arguments:
$name - The name of the schema/field being filtered
$value - The value to filter, either the entire item, or a single field
$conf - The configuration for the schema/field
@params - Other parameters if configured
For example, here is a filter that will run a password through a one-way hash digest:
use Digest;
my $digest = sub {
my ( $field_name, $field_value, $field_conf ) = @_;
my $type = $field_conf->{ 'x-digest' }{ type };
Digest->new( $type )->add( $field_value )->b64digest;
};
$c->yancy->filter->add( 'digest' => $digest );
And you configure this on a field using x-filter
and x-digest
:
# mysite.conf
{
schema => {
users => {
properties => {
username => { type => 'string' },
password => {
type => 'string',
format => 'password',
'x-filter' => [ 'digest' ], # The name of the filter
'x-digest' => { # Filter configuration
type => 'SHA-1',
},
},
},
},
},
}
The same filter, but also configurable with extra parameters:
my $digest = sub {
my ( $field_name, $field_value, $field_conf, @params ) = @_;
my $type = ( $params[0] || $field_conf->{ 'x-digest' } )->{ type };
Digest->new( $type )->add( $field_value )->b64digest;
$field_value . $params[0];
};
$c->yancy->filter->add( 'digest' => $digest );
The alternative configuration:
# mysite.conf
{
schema => {
users => {
properties => {
username => { type => 'string' },
password => {
type => 'string',
format => 'password',
'x-filter' => [ [ digest => { type => 'SHA-1' } ] ],
},
},
},
},
}
Schemas can also have filters. A schema filter will get the entire hash reference as its value. For example, here's a filter that updates the last_updated
field with the current time:
$c->yancy->filter->add( 'timestamp' => sub {
my ( $schema_name, $item, $schema_conf ) = @_;
$item->{last_updated} = time;
return $item;
} );
And you configure this on the schema using x-filter
:
# mysite.conf
{
schema => {
people => {
'x-filter' => [ 'timestamp' ],
properties => {
name => { type => 'string' },
address => { type => 'string' },
last_updated => { type => 'datetime' },
},
},
},
}
You can configure filters on OpenAPI operations' inputs. These will probably want to operate on hash-refs as in the schema-level filters above. The config passed will be an empty hash. The filter can be applied to either or both of the path, or the individual operation, and will be executed in that order. E.g.:
# mysite.conf
{
openapi => {
definitions => {
people => {
properties => {
name => { type => 'string' },
address => { type => 'string' },
last_updated => { type => 'datetime' },
},
},
},
paths => {
"/people" => {
# could also have x-filter here
"post" => {
'x-filter' => [ 'timestamp' ],
# ...
},
},
}
},
}
You can also configure filters on OpenAPI operations' outputs, this time with the key x-filter-output
. Again, the config passed will be an empty hash. The filter can be applied to either or both of the path, or the individual operation, and will be executed in that order. E.g.:
# mysite.conf
{
openapi => {
paths => {
"/people" => {
'x-filter-output' => [ 'timestamp' ],
# ...
},
}
},
}
Supplied filters
These filters are always installed.
yancy.from_helper
The first configured parameter is the name of an installed Mojolicious helper. That helper will be called, with any further supplied parameters, and the return value will be used as the value of that field / item. E.g. with this helper:
$app->helper( 'current_time' => sub { scalar gmtime } );
This configuration will achieve the same as the above with last_updated
:
# mysite.conf
{
schema => {
people => {
properties => {
name => { type => 'string' },
address => { type => 'string' },
last_updated => {
type => 'datetime',
'x-filter' => [ [ 'yancy.from_helper' => 'current_time' ] ],
},
},
},
},
}
yancy.overlay_from_helper
Intended to be used for "items" rather than individual fields, as it will only work when the "value" parameter is a hash-ref.
The configured parameters are supplied in pairs. The first item in the pair is the string key in the hash-ref. The second is either the name of a helper, or an array-ref with the first entry as such a helper-name, followed by parameters to pass that helper. For each pair, the helper will be called, and its return value set as the relevant key's value. E.g. with this helper:
$app->helper( 'current_time' => sub { scalar gmtime } );
This configuration will achieve the same as the above with last_updated
:
# mysite.conf
{
schema => {
people => {
'x-filter' => [
[ 'yancy.overlay_from_helper' => 'last_updated', 'current_time' ]
],
properties => {
name => { type => 'string' },
address => { type => 'string' },
last_updated => { type => 'datetime' },
},
},
},
}
yancy.wrap
The configured parameters are a list of strings. For each one, the original value will be wrapped in a hash with that string as the key, and the previous value as the value. E.g. with this config:
'x-filter-output' => [
[ 'yancy.wrap' => qw(user login) ],
],
The original value of say { user =
'bob', password => 'h12' }> will become:
{
login => {
user => { user => 'bob', password => 'h12' }
}
}
The utility of this comes from being able to expressively translate to and from a simple database structure to a situation where simple values or JSON objects need to be wrapped in objects one or two deep.
yancy.unwrap
This is the converse of the above. The configured parameters are a list of strings. For each one, the original value (a hash-ref) will be "unwrapped" by looking in the given hash and extracting the value whose key is that string. E.g. with this config:
'x-filter' => [
[ 'yancy.unwrap' => qw(login user) ],
],
This will achieve the reverse of the transformation given in "yancy.wrap" above. Note that obviously the order of arguments is inverted, since this operates outside-inward, while yancy.wrap
operates inside-outward.
yancy.mask
Mask part of a field's value by replacing a regular expression match with the given character. The first parameter is a regular expression to match. The second parameter is the character to replace each matched character with.
# Replace all text before the @ with *
'x-filter' => [
[ 'yancy.mask' => '^[^@]+', '*' ]
],
# Replace all but the last two characters before the @
'x-filter' => [
[ 'yancy.mask' => '^[^@]+(?=[^@]{2}@)', '*' ]
],
yancy.filter.apply
my $filtered_data = $c->yancy->filter->apply( $schema, $item_data );
Run the configured filters on the given $item_data
. $schema
is a schema name. Returns the hash of $filtered_data
.
The property-level filters will run before any schema-level filter, so that schema-level filters can take advantage of any values set by the inner filters.
yancy.filters
Returns a hash-ref of all configured helpers, mapping the names to the code-refs.
yancy.schema
my $schema = $c->yancy->schema( $name );
$c->yancy->schema( $name => $schema );
my $schemas = $c->yancy->schema;
Get or set the JSON schema for the given schema $name
. If no schema name is given, returns a hashref of all the schema.
log_die
Raise an exception with "raise" in Mojo::Exception, first logging using "fatal" in Mojo::Log (through the log
helper.
TEMPLATES
This plugin uses the following templates. To override these templates with your own theme, provide a template with the same name. Remember to add your template paths to the beginning of the list of paths to be sure your templates are found first:
# Mojolicious::Lite
unshift @{ app->renderer->paths }, 'template/directory';
unshift @{ app->renderer->classes }, __PACKAGE__;
# Mojolicious
sub startup {
my ( $app ) = @_;
unshift @{ $app->renderer->paths }, 'template/directory';
unshift @{ $app->renderer->classes }, __PACKAGE__;
}
- layouts/yancy.html.ep
-
This layout template surrounds all other Yancy templates. Like all Mojolicious layout templates, a replacement should use the
content
helper to display the page content. Additionally, a replacement should usecontent_for 'head'
to add content to thehead
element.
SEE ALSO
AUTHOR
Doug Bell <preaction@cpan.org>
COPYRIGHT AND LICENSE
This software is copyright (c) 2021 by Doug Bell.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.