Group
Extension

PONAPI-Server/lib/PONAPI/DAO.pm

# ABSTRACT: Data Abstraction Object class
package PONAPI::DAO;

use Moose;

use PONAPI::DAO::Request::Retrieve;
use PONAPI::DAO::Request::RetrieveAll;
use PONAPI::DAO::Request::RetrieveRelationships;
use PONAPI::DAO::Request::RetrieveByRelationship;
use PONAPI::DAO::Request::Create;
use PONAPI::DAO::Request::CreateRelationships;
use PONAPI::DAO::Request::Update;
use PONAPI::DAO::Request::UpdateRelationships;
use PONAPI::DAO::Request::Delete;
use PONAPI::DAO::Request::DeleteRelationships;

has repository => (
    is       => 'ro',
    does     => 'PONAPI::Repository',
    required => 1,
);

has version => (
    is       => 'ro',
    isa      => 'Str',
    required => 1,
);

has json => (
    is      => 'ro',
    isa     => JSON::MaybeXS::JSON(),
    default => sub { JSON::MaybeXS->new->allow_nonref->utf8->canonical },
);

sub retrieve_all             { shift->_action( 'PONAPI::DAO::Request::RetrieveAll'            , @_ ) }
sub retrieve                 { shift->_action( 'PONAPI::DAO::Request::Retrieve'               , @_ ) }
sub retrieve_relationships   { shift->_action( 'PONAPI::DAO::Request::RetrieveRelationships'  , @_ ) }
sub retrieve_by_relationship { shift->_action( 'PONAPI::DAO::Request::RetrieveByRelationship' , @_ ) }
sub create                   { shift->_action( 'PONAPI::DAO::Request::Create'                 , @_ ) }
sub create_relationships     { shift->_action( 'PONAPI::DAO::Request::CreateRelationships'    , @_ ) }
sub update                   { shift->_action( 'PONAPI::DAO::Request::Update'                 , @_ ) }
sub update_relationships     { shift->_action( 'PONAPI::DAO::Request::UpdateRelationships'    , @_ ) }
sub delete : method          { shift->_action( 'PONAPI::DAO::Request::Delete'                 , @_ ) }
sub delete_relationships     { shift->_action( 'PONAPI::DAO::Request::DeleteRelationships'    , @_ ) }

sub _action {
    my $self         = shift;
    my $action_class = shift;

    my $ponapi_parameters = @_ == 1 ? $_[0] : +{ @_ };
    $ponapi_parameters->{repository} = $self->repository;
    $ponapi_parameters->{version}    = $self->version;
    $ponapi_parameters->{json}       = $self->json;

    local $@;
    my @ret;
    eval {
        @ret = $action_class->new($ponapi_parameters)->execute();
        1;
    } or do {
        my $e = $@ || 'Unknown error';
        @ret = PONAPI::Exception
                    ->new_from_exception($e)
                    ->as_response;
    };

    return @ret;
}

__PACKAGE__->meta->make_immutable;
no Moose; 1;

__END__

=pod

=encoding UTF-8

=head1 NAME

PONAPI::DAO - Data Abstraction Object class

=head1 VERSION

version 0.003003

=head1 SYNOPSIS

    use PONAPI::DAO;
    my $dao = PONAPI::DAO->new( repository => $repository );

    my ($status, $doc) = $dao->retrieve( type => $type, id => $id );
    die "retrieve failed; status $status, $doc->{errors}[0]{detail}"
        if $doc->{errors};

    use Data::Dumper;
    say Dumper($doc->{data});

    # Fetch all resources of this type
    $dao->retrieve_all( type => $type );

    # Fetch all the relationships of $rel_type for the requested resource
    $dao->retrieve_relationships(
        type     => $type,
        id       => $id,
        rel_type => $rel_type,
    );

    # Like the above, but fetches full resources instead of just relationships
    $dao->retrieve_by_relationship(
        type     => $type,
        id       => $id,
        rel_type => $rel_type,
    );

    # Create a new resource
    $dao->create(
        type => $type,
        data => {
            type          => $type,
            attributes    => { ... },
            relationships => { ... },
        }
    );

    # *Add* a new entry to the relationships between $type and $rel_type
    $dao->create_relationsips(
        type     => $type,
        rel_type => $rel_type,
        data => [
            { ... },
        ]
    );

    # Update the attributes and/or relationships of a resource
    $dao->update(
        type => $type,
        id   => $id,
        data => {
            type          => $type,
            id            => $id,
            attributes    => { ... },
            relationships => { ... },
        },
    );

    # Update the relationships of a given type for one resource
    $dao->update_relationships(
        type     => $type,
        id       => $id,
        rel_type => $rel_type,
        data     => $update_data,
    );

    # Delete a resource
    $dao->delete(
        type => $type,
        id   => $id,
    );

    # Delete the members from the relationship
    $dao->delete_relationships(
        type     => $type,
        id       => $id,
        rel_type => $rel_type,
        data     => [
            { ... }, ...
        ],
    );

=head1 DESCRIPTION

Data Access Object for the JSON API.  This sits in between a server
and a L<repository|"PONAPI::Repository">.

All public DAO methods will return a 3-item list of a status, headers,
and the response body; this can then be fed directly to a PSGI application:

If present, the C<data> key of the response will contain either a
resource, or an arrayref of resources.  Resources are represented as
plain hashrefs, and they B<must> include both a C<type> and C<id>; they
may also contain additional keys.  See L<http://jsonapi.org/format/#document-resource-objects>
for a more in-depth description.

=head1 METHODS

=head2 new

Create a new instance of PONAPI::DAO.

    my $DAO = PONAPI::DAO->new(
        repository => $repository,
    );

Where C<$repository> implements the L<PONAPI::Repository> role.

As expanded below in L</"Return value of update operations">, the JSON API specification requires some
update operations returning C<200 OK> to also do a C<retrieve> and include
it in the response.
By default, C<PONAPI::DAO> will simply turn those C<200 OK> into
C<202 Accepted>, avoiding the need to do the extra fetch.  If needed, the
full 200 responses can be re-enabled by passing
C<respond_to_updates_with_200 =E<gt> 1,> to C<new>.

=head1 API METHODS

With the exception of C<create> and C<retrieve_all>, the type and id arguments are mandatory
for all operations.

=head2 retrieve

Retrieve a resource.  Returns both the status of the request and
the document to be encoded.

    my ( $status, $doc ) = $dao->retrieve( type => "articles", id => 1 );

    if ( $doc->{errors} ) {
        die "Welp! Got some errors: ", join "\n",
                map $_->{detail}, @{ $doc->{errors} };
    }

    say $doc->{data}{attributes}{title};

This accepts several optional values:

=over 4

=item fields

Allows fetching only specific fields of the resource:

    # This will fetch the entire resource
    $dao->retrieve(type => "articles", id => 1);

    # This will only fetch the title attribute
    $dao->retrieve(
        type   => "articles",
        id     => 1,
        fields => { articles => [qw/ title /] },
    );

Note how the fields fetched are requested per attribute type.  This allows you to
request specific fields in resources fetched through C<include>.

=item include

Allows including related resources.

    # The response will contain a top-level 'include' key with the
    # article's author
    $dao->retrieve(type => "articles", id => 1, include => [qw/ author /]);

    # We can combine include with C<fields> to fetch just the author's name:
    my $response = $dao->retrieve(
        id      => 1,
        type    => "articles",
        include => [qw/ author /],
        fields  => { author => [qw/ name /] }
    );

These will show up in the document in the top-level "included" key.

=item page

Used to provide pagination information to the underlaying repository.
Each implementation may provide a different pagination strategy.

=item filter

Entirely implementation-specific.

=back

=head2 retrieve_all

As you might expect, this is similar to C<retrieve>.  The returned document
will contain an arrayref of resource, rather than a single resource.

Depending on the implementation, you may be able to combine this with
C<filter> to retrieve multiple specific resources in a single request.

C<retrieve_all> takes all the same optional arguments as C<retrieve>,
plus one of its own:

=over 1

=item sort

Sorting strategy for the request.  Implementation-specific.

=back

=head2 retrieve_relationships

This retrieves all relationships of C<$type>.  Will return either an
arrayref or a hashref, depending on whether the requested relationship is
one-to-one or one-to-many:

    # Retrieves all comments made for an article
    $doc = $dao->retrieve_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "comments",
    );
    # articles-to-comments is one-to-many, so it returns an arrayref
    say scalar @{ $doc->{data} };

    $doc = $dao->retrieve_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "author",
    );
    # articles-to-author is one-to-one, so it returns a hashref
    say $doc->{data}{id};

Takes two optional arguments, C<filter> and C<page>; both are entirely
implementation specific.

=head2 retrieve_by_relationships

Like C<retrieve_relationships>, but fetches full resources, rather than
identifier objects.

    # One-to-many relationship, this returns an arrayref of resource hashrefs.
    $dao->retrieve_by_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "comments",
    );
    # Same as:
    $doc      = $dao->retrieve_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "comments",
    );
    $comments = $dao->retrieve_all(
        type   => $doc->{data}{type},
        filter => { id => [ map $_->{id}, @{ $doc->{data} } ] },
    );

    # One-to-one relationship
    $doc = $dao->retrieve_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "author",
    );
    # Same as:
    $doc    = $dao->retrieve_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "author",
    );
    $author = $dao->retrieve(
        type => $doc->{data}{type},
        id   => $doc->{data}{id},
    );

Takes the same optional arguments as C<retrieve> and C<retrieve_all>, whichever is applicable.

=head2 delete

Deletes a resource.

    $dao->delete( type => "articles", id => 1 );

May or may not return a document with a top-level meta key.

=head2 create

Creates a resource.

    $dao->create(
        type => "articles",
        data => {
            type          => "articles",
            attributes    => { ... },
            relationships => { ... },
        },
    );

This is one of the few methods where the C<id> is optional.  If provided, the underlaying
implementation may choose to use it, instead of generating a new idea for the created resource.

If successful, the response will include both a C<Location> header specifying
where the new resource resides, and a document that includes the newly
created resource.

=head2 update

Updates a resource.  This can be used to either update the resource attributes,
or its relationships; for the latter, you may want to consider using C<update_relationships>,
C<create_relationships>, or C<delete_relationships> instead.

    # Change article's title
    $dao->update(
        type => "articles",
        id   => 1,
        data => {
            type       => "articles",
            id         => 1,
            attributes => { title => "Updated title!" },
        }
    );

    # Change the article's author
    $dao->update(
        type => "articles",
        id   => 1,
        data => {
            type          => "articles",
            id            => 1,
            relationships => {
                author => { type => "people", id => 99 },
            },
        },
    );

    # Switch the tags of the article to a new set of tags
    $dao->update(
        type => "articles",
        id   => 1,
        data => {
            type          => "articles",
            id            => 1,
            relationships => {
                tags => [
                    { type => "tag", id => 4 },
                    { type => "tag", id => 5 },
                ],
            },
        },
    );

Missing attributes or relationships will B<not> be modified.

=head3 Return value of update operations

C<update>, C<delete_relationships>, C<create_relationships>, and
C<update_relationships> all follow the same rules for their responses.

If successful, they will return with either:

=over 3

=item 200 OK

If the update was successful and no extra data was updated, the response
will include a top-level C<meta> key, with a description of what was
updated.

Meanwhile, if the update was successful but more data than requested was
updated -- Consider C<updated-at> columns in a table -- then the request
will return both a top-level C<meta> key, and a top-level C<data> key,
containing the results of a C<retrieve> operation on the primary updated
resource.  Since this behavior can be undesirable, unless C<PONAPI::DAO-E<gt>new>
was passed C<respond_to_updates_with_200 =E<gt> 1>, this sort of response is
disabled, and the server will instead respond with a L</"202 Accepted">,
described below.

=item 202 Accepted

The response will include a top-level C<meta> key, with a human-readable
description of the success.

This is used when the server accepted the operation, but hasn't yet
completed it;  "Completed" being purposely very ambiguous.  In a SQL-based
implementation, it might simply mean that the change hasn't fully replicated
yet.

=item 204 No Content

If the operation was successful and nothing beyond the requested was modified,
the server may choose to send a 204 with no body, instead of a 200.

=back

=head2 delete_relationships

Remove members from a one-to-many relationship.

    # Remove two comments from the article
    $dao->delete(
        type     => "articles",
        id       => 1,
        rel_type => "comments',
        data     => [
            { type => "comment", id => 44 },
            { type => "comment", id => 89 },
        ],
    );

See also L</"Return value of update operations">.

=head2 update_relationships

Update the relationships of C<$rel_type>; this will replace all relationships
of the requested type with the ones provided.  Note that different semantics
are used for one-to-one and one-to-many relationships:

    # Replace all comments
    $dao->update_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "comments",
        # articles-to-comments is one-to-many, so it gets an arrayref
        data     => [ { ... }, { ... } ],
    );

    # Change the author of the article
    $dao->update_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "author",
        # articles-to-authors is one-to-one, so it gets a simple hashref
        data     => { type => "people", id => 42 },
    );

    # Clear the comments of an article
    $dao->update_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "comments",
        # Empty array to clear out a one-to-many
        data     => [],
    );

    # Clear the author of the relationship
    $dao->update_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "author",
        # undef to clear out one-to-one
        data     => undef,
    );

See also L</"Return value of update operations">.

=head2 create_relationships

Adds a new member to the specified one-to-many relationship.

    # Add a new, existing comment to the article
    $dao->create_relationships(
        type     => "articles",
        id       => 1,
        rel_type => "comments",
        data     => [
            { type => "comment", id => 55 },
        ],
    );

See also L</"Return value of update operations">.

=head1 AUTHORS

=over 4

=item *

Mickey Nasriachi <mickey@cpan.org>

=item *

Stevan Little <stevan@cpan.org>

=item *

Brian Fraser <hugmeir@cpan.org>

=back

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2019 by Mickey Nasriachi, Stevan Little, Brian Fraser.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut


Powered by Groonga
Maintained by Kenichi Ishigaki <ishigaki@cpan.org>. If you find anything, submit it on GitHub.