NAME

PONAPI::DAO - Data Abstraction Object class

VERSION

version 0.003003

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     => [
        { ... }, ...
    ],
);

DESCRIPTION

Data Access Object for the JSON API. This sits in between a server and a 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 data key of the response will contain either a resource, or an arrayref of resources. Resources are represented as plain hashrefs, and they must include both a type and id; they may also contain additional keys. See http://jsonapi.org/format/#document-resource-objects for a more in-depth description.

METHODS

new

Create a new instance of PONAPI::DAO.

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

Where $repository implements the PONAPI::Repository role.

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

API METHODS

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

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:

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 include.

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.

page

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

filter

Entirely implementation-specific.

retrieve_all

As you might expect, this is similar to 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 filter to retrieve multiple specific resources in a single request.

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

sort

Sorting strategy for the request. Implementation-specific.

retrieve_relationships

This retrieves all relationships of $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, filter and page; both are entirely implementation specific.

retrieve_by_relationships

Like 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 retrieve and retrieve_all, whichever is applicable.

delete

Deletes a resource.

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

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

create

Creates a resource.

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

This is one of the few methods where the 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 Location header specifying where the new resource resides, and a document that includes the newly created resource.

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 update_relationships, create_relationships, or 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 not be modified.

Return value of update operations

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

If successful, they will return with either:

200 OK

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

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

202 Accepted

The response will include a top-level 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.

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.

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 "Return value of update operations".

update_relationships

Update the relationships of $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 "Return value of update operations".

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 "Return value of update operations".

AUTHORS

  • Mickey Nasriachi <mickey@cpan.org>

  • Stevan Little <stevan@cpan.org>

  • Brian Fraser <hugmeir@cpan.org>

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.