App-SpamcupNG

 view release on metacpan or  search on metacpan

lib/App/SpamcupNG/Summary/Recorder.pm  view on Meta::CPAN

    my $recorder = App::SpamcupNG::Summary::Recorder->new(
        '/some/path/database_file' );
    $recorder->init;
    $recorder->save($summary);

=head1 DESCRIPTION

This class is used to persist L<App::SpamcupNG::Summary> instances to a SQLite3
database.

=head1 METHODS

=head2 new

Creates a new recorder instance.

Expects as parameter the complete path to a existing (or to create) SQLite 3
file.

=cut

sub new {
    my ( $class, $file, $now ) = @_;
    my $self = { db_file => $file, now => $now };

    # TODO: add tables names for DRY also replacing in _save_attrib
    $self->{dbh} = DBI->connect( ( 'dbi:SQLite:dbname=' . $file ), '', '' )
      or confess $DBI::errstr;
    bless $self, $class;
    return $self;
}

=head2 init

Initialize the database if it doesn't exist yet. This is idempotent.

=cut

sub init {
    my $self = shift;
    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS email_content_type (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE
)
    }
    ) or confess $self->{dbh}->errstr;

    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS spam_age_unit (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE
)
    }
    ) or confess $self->{dbh}->errstr;

    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS email_charset (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE
)
    }
    ) or confess $self->{dbh}->errstr;

    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS receiver (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  email TEXT NOT NULL UNIQUE
)
    }
    ) or confess $self->{dbh}->errstr;

    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS mailer (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  name TEXT NOT NULL UNIQUE
)
    }
    ) or confess $self->{dbh}->errstr;

    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS summary (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  tracking_id TEXT NOT NULL UNIQUE,
  created INTEGER NOT NULL,
  charset_id INTEGER REFERENCES email_charset ON DELETE SET NULL,
  content_type_id INTEGER REFERENCES email_content_type ON DELETE SET NULL,
  age INTEGER NOT NULL,
  age_unit_id INTEGER REFERENCES spam_age_unit ON DELETE SET NULL,
  mailer_id INTEGER REFERENCES mailer ON DELETE SET NULL
)
    }
    ) or confess $self->{dbh}->errstr;

    $self->{dbh}->do(
        q{
CREATE TABLE IF NOT EXISTS summary_receiver (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  summary_id INTEGER REFERENCES summary ON DELETE CASCADE,
  receiver_id INTEGER REFERENCES receiver ON DELETE CASCADE,
  report_id TEXT UNIQUE
)
    }
    ) or confess $self->{dbh}->errstr;

}

=head2 save

Persists a L<App::SpamcupNG::Summary> instance to the database.

Returns "true" (in Perl terms) if everything goes fine.

=cut

sub save {
    my ( $self, $summary ) = @_;
    my $summary_class = 'App::SpamcupNG::Summary';
    my $ref           = ref($summary);
    confess "summary must be instance of $summary_class class, not '$ref'"
      unless ( $ref eq $summary_class );

    # TODO: create a method for Summary to provide those names
    my @fields = qw(content_type age_unit charset mailer);
    my %fields;

    foreach my $field_name (@fields) {
        my $method = "get_$field_name";
        $fields{$field_name} =
          $self->_save_attrib( $field_name, $summary->$method );
    }

    lock_hash(%fields);

    my $summary_id = $self->_save_summary( $summary, \%fields );

    foreach my $receiver ( @{ $summary->get_receivers } ) {
        my $receiver_id = $self->_save_attrib( 'receiver', $receiver->email );
        $self->_save_sum_rec( $summary_id, $receiver_id, $receiver->report_id );
    }

    return 1;
}

sub _save_sum_rec {
    my ( $self, $sum_id, $rec_id, $report_id ) = @_;
    my @values = ( $sum_id, $rec_id, $report_id );
    $self->{dbh}->do(
        q{
INSERT INTO summary_receiver (summary_id, receiver_id, report_id)
VALUES(?, ?, ?)
        }, undef, @values
    ) or confess $self->{dbh}->errstr;
}

sub _save_summary {
    my ( $self, $summary, $fields_ref ) = @_;
    my $now    = $self->{now} ? $self->{now} : DateTime->now->epoch;
    my $insert = q{
INSERT INTO summary
(tracking_id, created, charset_id, content_type_id, age, age_unit_id, mailer_id)
VALUES (?, ?, ?, ?, ?, ?, ?)
};
    my @values = (
        $summary->get_tracking_id, $now,
        $fields_ref->{charset},    $fields_ref->{content_type},
        $summary->get_age,         $fields_ref->{age_unit},
        $fields_ref->{mailer}
    );
    $self->{dbh}->do( $insert, undef, @values )
      or confess $self->{dbh}->errstr;
    return $self->{dbh}->last_insert_id;
}

sub _save_attrib {
    my ( $self, $attrib, $value ) = @_;
    my %attrib_to_table = (
        content_type => 'email_content_type',
        age_unit     => 'spam_age_unit',
        charset      => 'email_charset',
        mailer       => 'mailer',
        receiver     => 'receiver'
    );

    return undef unless ( defined($value) );
    confess "'$attrib' is not a valid attribute"
      unless ( exists( $attrib_to_table{$attrib} ) );
    my $table = $attrib_to_table{$attrib};
    my $column;

    if ( $attrib eq 'receiver' ) {
        $column = 'email';
    }
    else {
        $column = 'name';
    }

    my $row_ref =
      $self->{dbh}
      ->selectrow_arrayref( "SELECT id FROM $table WHERE $column = ?",
        undef, $value );
    return $row_ref->[0] if ( defined( $row_ref->[0] ) );

    $self->{dbh}->do(
        qq{
INSERT INTO $table ($column) VALUES (?)
        },
        undef,
        $value
    );

    return $self->{dbh}->last_insert_id;
}

=head2 DESTROY

Properly closes the SQLite 3 database file when the recorder instance goes out
of scope.

=cut

sub DESTROY {
    my $self = shift;

    if ( $self->{dbh} ) {
        $self->{dbh}->disconnect or warn $self->{dbh}->errstr;
    }
}

=pod

=head1 QUERYING RESULTS

This is a sample query to checkout records in the database:

    SELECT A.id,
      A.tracking_id,
      DATETIME(A.created, 'unixepoch') AS CREATED,
      B.name AS CHARSET,
      C.name AS CONTENT_TYPE,
      A.age,
      D.name AS MAILER,
      E.report_id,
      F.email
    FROM summary A outer left join email_charset B on A.charset_id = B.id
    INNER JOIN email_content_type C ON A.content_type_id = C.id
    OUTER LEFT JOIN mailer D ON A.mailer_id = D.id
    INNER JOIN summary_receiver E ON A.id = E.summary_id
    INNER JOIN receiver F ON E.receiver_id = F.id;

=head1 SEE ALSO

=over

=item *

L<https://www.sqlite.org/docs.html>

=back

=cut

1;



( run in 0.877 second using v1.01-cache-2.11-cpan-5a3173703d6 )