Skip to content

Instantly share code, notes, and snippets.

@renatocron
Created November 9, 2018 09:24
Show Gist options
  • Save renatocron/12c780411d2b136800061977ded3b3bf to your computer and use it in GitHub Desktop.
Save renatocron/12c780411d2b136800061977ded3b3bf to your computer and use it in GitHub Desktop.

Right now, my controllers looks like controller-user.pm

Then, at Resultset/User.pm, it has two roles, one of them, is Nutrinet::Role::Verification

At Nutrinet::Role::Verification it add the method 'execute' that is called from controller.

it prepare the Data::Verifier to run the verifiers_specs and if it pass, it run the action_specs for the for using the params from with

package Nutrinet::Controller::User::Me;
use Mojo::Base 'Nutrinet::Controller';
use JSON;
sub get {
my $c = shift;
my $user = $c->stash->{user_rs}->next or $c->user_not_found;
$c->render(
json => $user->as_row( include_survey => $c->req->param('include_survey') ),
status => 200,
);
}
sub put {
my $c = shift;
my $user = $c->stash->{user_rs}->next or $c->user_not_found;
$c->req->params->append(
log_data => to_json(
{
remote_addr => $c->remote_addr
}
)
);
$user = $user->execute( $c, for => 'update', with => $c->req->params );
$c->render(
json => $user->as_row,
status => 200,
);
}
1;
#<<<
use utf8;
package Nutrinet::Schema::Result::User;
# Created by DBIx::Class::Schema::Loader
# DO NOT MODIFY THE FIRST PART OF THIS FILE
use strict;
use warnings;
use Moose;
use MooseX::NonMoose;
use MooseX::MarkAsMethods autoclean => 1;
extends 'DBIx::Class::Core';
__PACKAGE__->load_components("InflateColumn::DateTime", "TimeStamp");
__PACKAGE__->table("user");
__PACKAGE__->add_columns(
"id",
{
data_type => "integer",
is_auto_increment => 1,
is_nullable => 0,
sequence => "nutrinet.user_id_seq",
},
"email",
{
data_type => "text",
is_nullable => 0,
original => { data_type => "varchar" },
},
"name",
{
data_type => "text",
is_nullable => 0,
original => { data_type => "varchar" },
},
"gender",
{
data_type => "text",
is_nullable => 0,
original => { data_type => "varchar" },
},
"birthdate",
{ data_type => "date", is_nullable => 0 },
"created_at",
{
data_type => "timestamp with time zone",
default_value => \"current_timestamp",
is_nullable => 0,
original => { default_value => \"now()" },
},
"terms_checked_at",
{ data_type => "timestamp with time zone", is_nullable => 0 },
"survey_start_date",
{ data_type => "timestamp with time zone", is_nullable => 1 },
"email_validated_at",
{ data_type => "timestamp with time zone", is_nullable => 1 },
"optout",
{ data_type => "boolean", is_nullable => 0 },
"active",
{ data_type => "boolean", default_value => \"true", is_nullable => 0 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->has_many(
"user_logs",
"Nutrinet::Schema::Result::UserLog",
{ "foreign.user_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"user_one_time_passwords",
"Nutrinet::Schema::Result::UserOneTimePassword",
{ "foreign.user_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"user_sessions",
"Nutrinet::Schema::Result::UserSession",
{ "foreign.user_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
__PACKAGE__->has_many(
"user_surveys",
"Nutrinet::Schema::Result::UserSurvey",
{ "foreign.user_id" => "self.id" },
{ cascade_copy => 0, cascade_delete => 0 },
);
#>>>
# Created by DBIx::Class::Schema::Loader v0.07049 @ 2018-11-05 16:46:56
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MKTTUeCmi4+mf55gtgO+jw
with 'Nutrinet::Role::Verification';
with 'Nutrinet::Role::Verification::TransactionalActions::DBIC';
use JSON;
use Data::Verifier;
use MooseX::Types::Email qw/EmailAddress/;
use MooseX::Types::Common::Numeric qw/PositiveInt/;
use Nutrinet::Types qw/DateStr/;
sub verifiers_specs {
my $self = shift;
return {
update => Data::Verifier->new(
filters => [qw(trim)],
profile => {
name => {
required => 0,
max_length => 128,
type => 'Str',
},
log_data => {
required => 1,
max_length => 2048,
type => 'Str',
},
birthdate => {
required => 0,
type => DateStr
},
gender => {
required => 0,
max_length => 10,
type => 'Str',
post_check => sub {
my $r = shift;
return 0 unless $r->get_value('gender') =~ /^(male|female)$/;
return 1;
}
},
email => {
required => 0,
max_length => 64,
type => EmailAddress,
post_check => sub {
my $r = shift;
die \[ 'email', 'already-in-use' ]
if ( $self->find( { active => 1, email => lc $r->get_value('email') } ) );
return 1;
}
},
},
),
};
}
sub action_specs {
my $self = shift;
return {
update => sub {
my %values = shift->valid_values;
not defined $values{$_} and delete $values{$_} for keys %values;
my $log_data = delete $values{log_data};
return $self unless keys %values;
my $user = $self->update( \%values );
$user->log( 'user.update', $log_data );
$user->discard_changes;
return $user;
}
};
}
sub log {
my ( $self, $context, $data ) = @_;
$data = from_json($data) if ref $data eq '';
my $ip = delete $data->{remote_addr};
$self->user_logs->create(
{
context => $context,
data => to_json($data),
ip_address => $ip,
}
);
}
sub new_session {
my ($self) = @_;
my $session = $self->user_sessions->create(
{
api_key => \'uuid_generate_v4()'
}
);
$session->discard_changes;
return {
jti => $session->api_key,
exp => int( $session->valid_until->epoch ),
typ => 'usr',
uid => $self->id,
};
}
sub as_row {
my ($self, %opts) = @_;
my $surveys;
if ($opts{include_survey}){
$surveys = [ map { $_->as_row } $self->list_surveys ];
}
return {
user => {
id => 0 + $self->id,
( map { $_ => $self->$_ || '' } qw/name email gender birthdate/ ),
},
($surveys ? (surveys => $surveys) : ()),
};
}
sub add_pending_surveys {
my ($self ) = @_;
# todo: begin database, call obtain_lock, insert pending surveys..
};
sub obtain_lock {
my ( $self ) = @_;
## right here, that's where I stopped!
log_info( 'obtain_lock start ' . $self->id );
my ($lock_myself) =
$self->result_source->schema->resultset('User')
->search( { 'me.id' => $self->id }, { for => 'UPDATE', columns => ['id'] } )->next;
log_info( 'obtain_lock end ' . $self->id );
return $lock_myself;
}
sub list_surveys {
my ($self ) = @_;
my $user = $self->schema->resultset('User')->search({
'me.id' => $self->id,
}, {
prefetch => { 'user_surveys' => 'survey_config'},
order_by => 'user_surveys.visible_after'
})->next;
return map { $_ } $user->user_surveys;
};
# You can replace this text with custom code or comments, and it will be preserved on regeneration
__PACKAGE__->meta->make_immutable;
1;
package Nutrinet::Schema::ResultSet::User;
use namespace::autoclean;
use utf8;
use Moose;
extends 'DBIx::Class::ResultSet';
with 'Nutrinet::Role::Verification';
with 'Nutrinet::Role::Verification::TransactionalActions::DBIC';
use JSON;
use Data::Verifier;
use MooseX::Types::Email qw/EmailAddress/;
use MooseX::Types::Common::Numeric qw/PositiveInt/;
use Nutrinet::Types qw/DateStr/;
sub verifiers_specs {
my $self = shift;
return {
create => Data::Verifier->new(
filters => [qw(trim)],
profile => {
name => {
required => 1,
max_length => 128,
type => 'Str',
},
log_data => {
required => 1,
max_length => 2048,
type => 'Str',
},
birthdate => {
required => 1,
type => DateStr
},
terms_checked_at => {
required => 1,
type => DateStr
},
gender => {
required => 1,
max_length => 10,
type => 'Str',
post_check => sub {
my $r = shift;
return 0 unless $r->get_value('gender') =~ /^(male|female)$/;
return 1;
}
},
email => {
required => 1,
max_length => 64,
type => EmailAddress,
post_check => sub {
my $r = shift;
die \[ 'email', 'already-in-use' ]
if ( $self->find( { active => 1, email => lc $r->get_value('email') } ) );
return 1;
}
},
},
),
email_exists => Data::Verifier->new(
filters => [qw(trim)],
profile => {
email => {
required => 1,
max_length => 64,
type => EmailAddress,
},
}
),
};
}
sub action_specs {
my $self = shift;
return {
email_exists => sub {
my %values = shift->valid_values;
$values{email} = lc $values{email};
my $exists = $self->search( { email => $values{email}, active => 1 } )->next;
if ($exists) {
return { exists => JSON::true, };
}
return { exists => JSON::false };
},
create => sub {
my %values = shift->valid_values;
delete $values{password_confirm};
not defined $values{$_} and delete $values{$_} for keys %values;
$values{email} = lc $values{email};
$values{optout} = JSON::false;
my $log_data = delete $values{log_data};
my $user = $self->create( \%values );
$user->log('user.create', $log_data );
my $session = $user->new_session;
$user->add_pending_surveys();
return {
id => $user->id,
session => $session
};
}
};
}
1;
package Nutrinet::Role::Verification;
use namespace::autoclean;
use Moose::Role;
use Nutrinet::Data::Manager;
use Data::Diver qw(Dive);
has verifiers => (
is => 'ro',
isa => 'HashRef',
lazy_build => 1,
builder => 'verifiers_specs'
);
has actions => (
is => 'ro',
isa => 'HashRef',
lazy_build => 1,
builder => 'action_specs'
);
requires 'verifiers_specs';
requires 'action_specs';
sub check {
my ( $self, %args ) = @_;
my $path = delete $args{for};
my $input = delete $args{with};
if (ref $input eq 'Mojo::Parameters'){
$input = $input->to_hash;
}
my $verifier = Dive( $self->verifiers, split( /\./, $path ) );
my $action = Dive( $self->actions, split( /\./, $path ) );
return Nutrinet::Data::Manager->new(
input => $input,
verifiers => { $path => $verifier },
actions => { $path => $action }
);
}
sub execute {
my ( $self, $c, %args ) = @_;
my $dm = $self->check(%args);
my $result = $dm->apply;
unless ( $dm->success ) {
$c->render(
json => { form_error => $dm->errors, error => 'form_error' },
status => 400,
);
$c->detach();
}
return wantarray ? ( $dm, $result ) : $result;
}
1;
@renatocron
Copy link
Author

at controller, $c->stash->{user_rs}->next returns a Nutrinet::Schema::Result::User.

I forget to add the signup controller:

package Nutrinet::Controller::Unauthenticated::SignUp;
use Mojo::Base 'Nutrinet::Controller';

use JSON;

sub post {
    my $c = shift;

    $c->req->params->append(
        log_data => to_json(
            {
                remote_addr => $c->remote_addr
            }
        )
    );

    my $obj = $c->schema->resultset('User')->execute( $c, for => 'create', with => $c->req->params );

    $obj->{api_key} = $c->encode_jwt( delete $obj->{session} );

    $c->render(
        json   => $obj,
        status => 200,
    );
}

1;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment