NAME

Mojolicious::Plugin::Yancy - Embed a simple admin CMS into your Mojolicious application

VERSION

version 1.063

SYNOPSIS

use Mojolicious::Lite;
plugin Yancy => {
    backend => 'pg://postgres@/mydb',
    schema => { ... },
};

## With custom auth routine
use Mojo::Base 'Mojolicious';
sub startup {
    my ( $app ) = @_;
    my $auth_route = $app->routes->under( '/yancy', sub {
        my ( $c ) = @_;
        # ... Validate user
        return 1;
    } );
    $app->plugin( 'Yancy', {
        backend => 'pg://postgres@/mydb',
        schema => { ... },
        route => $auth_route,
    });
}

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 Yancy::Help::Config.

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.

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 use content_for 'head' to add content to the head element.

SEE ALSO

AUTHOR

Doug Bell <preaction@cpan.org>

COPYRIGHT AND LICENSE

This software is copyright (c) 2020 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.