The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

PONAPI::Manual - PONAPI - Perl implementation of {JSON:API} (http://jsonapi.org/) v1.0

VERSION

version 0.001000

DESCRIPTION

This document describes how to use and set up PONAPI::Server. The latter parts describe how to set up a manual from scratch, including a set-by-step of the implementation.

The recommended usage is to use PONAPI::Server directly as a PSGI application:

    # myapp.psgi
    use PONAPI::Server;
    PONAPI::Server->new(
        'repository.class' => 'Test::PONAPI::DAO::Repository::MockDB',
    )->to_app;

And later, on the command line, you can use any PSGI-compatible utility to run the application:

    plackup -p 5000 myapp.psgi

Any {json:api} compliant client can then access your application:

    $ perl -MPONAPI::Client -MData::Dumper -E 'say Dumper(PONAPI::Client->new->retrieve(type => "people", id => 88))'

    $ curl -X GET -H "Content-Type: application/vnd.api+json" 'http://0:5000/people/88

That'll give you the default options, with the default repository; for a real world scenario, you'll have to get your hands dirty.

Because {json:api} extensively uses the PATCH method, we recommend using Plack::Middleware::MethodOverride or another similar middleware to enable HTTP method overriding; this is to allow clients without PATCH to still use the API:

    # myapp.psgi
    use PONAPI::Server;
    use Plack::Middleware::MethodOverride;

    my $app = PONAPI::Server->to_app(
        'repository.class' => 'Test::PONAPI::DAO::Repository::MockDB',
    )
    Plack::Middleware::MethodOverride->wrap($app);

NAME

PONAPI::Manual - An introduction to using PONAPI::Server

SERVER CONFIGURATION

The server looks for a YAML configuration file in conf/server.yml, under the current working directory.

    # PONAPI server & repository configuration file

    # switch options take the positive values: "yes", 1 & "true"
    #                     and negative values: "no", 0 & "false"

    server:
      spec_version:            "1.0"        # {json:api} version
      sort_allowed:            "false"      # server-side sorting support
      send_version_header:     "true"       # server will send 'X-PONAPI-Server-Version' header responses
      send_document_self_link: "true"       # server will add a 'self' link to documents without errors
      links_type:              "relative"   # all links are either "relative" or "full" (inc. request base)
      respond_to_updates_with_200: "false"  # successful updates will return 200's instead of 202's when true

    repository:
      class:  "Test::PONAPI::DAO::Repository::MockDB"
      args:   []

Currently, only two sections of server.yml are relevant to PONAPI::Server: server, which influences how the server behaves, and repository, which is the class that controls how the data is acquired. See "CREATING A REPOSITORY" for more on the latter.

spec_version

The {json:api} version that the server is serving. Must be present.

sort_allowed

true/false. If true, server-side sorting is supported. If false, requests including the sort parameter will immediately return an error, without ever reaching the repository.

send_version_header

true/false. If true, all responses will include the {json:api} spec version set in spec_version, through a custom X-PONAPI-Server-Version header.

true/false. On successful operations, the document will be forced to include a links section with a self key, which will usually point to the request path (but not always -- consider pagination).

Either "relative" or "full" -- full links will include the request base (hostname), while relative requests will not:

    /articles/1                      # Relative link
    http://localhost:5000/articles/1 # Full link
respond_to_updates_with_200

true/false.

This is false by default, which will cause successful update-like operations (update, update_relationships, create_relationships, and delete_relationships) to immediately return with a 202, instead of a 200.

This is often desirable, because the specification mandates that certain update operations returning a 200 do an extra retrieve and pass over that information to the client. Due to several factors (e.g. replication delay) that retrieve might either need to be run on the master, or return unreliable information, and may slow down requests due to the extra fetch.

CREATING A REPOSITORY

The other relevant section of server.yml is repository, which may look something like this:

    repository:
      class:  "My::Repository::Yadda"
      args:   []

Where My::Repository::Yadda is a class that consumes the PONAPI::DAO::Repository role; args will be passed to the repo when it is instantiated.

To use PONAPI, we'll need a repo to communicate between the server and the data we want to server, so let's create a minimal repo from scratch. Our aim here is to have a repo that will handle something like a minimalistic blog, so we need to figure out what data types we'll handle, and what their relationships are.

For this example, we'll have three types -- articles, comments, and people -- and two relationships: articles have one or more comments, and articles have one author.

It may help to think of relationships as something like foreign keys in SQL.

In any case, the structure we want to have is this: articles -> has comments -> has an author (type people)

And in SQL, the tables might look something like this:

    CREATE TABLE articles (
        id            INTEGER     PRIMARY KEY AUTOINCREMENT,
        title         CHAR(64)    NOT NULL,
        body          TEXT        NOT NULL,
        created       DATETIME    NOT NULL   DEFAULT CURRENT_TIMESTAMP,
        updated       DATETIME    NOT NULL   DEFAULT CURRENT_TIMESTAMP
    );

    CREATE TABLE people (
        id            INTEGER     PRIMARY KEY,
        name          CHAR(64)    NOT NULL   DEFAULT "anonymous",
        age           INTEGER     NOT NULL   DEFAULT "100",
        gender        CHAR(10)    NOT NULL   DEFAULT "unknown"
    );

    CREATE TABLE comments (
        id            INTEGER     PRIMARY KEY,
        body          TEXT        NOT NULL DEFAULT ""
    );

And finally, on perl:

    # First, some boilerplate...
    package My::Repository::Hooray;
    use Moose;
    with 'PONAPI::DAO::Repository';
    use PONAPI::DAO::Constants;
    use PONAPI::DAO::Exception;

That is the initial boilerplate for any repo. The PONAPI::DAO::Repository role requires us to define several methods, which also require us to have some information regarding the data we are serving:

    my %types = (
        articles => 1,
        comments => 1,
        people   => 1,
    );
    # Returns true if the repo handles $type
    sub has_type {
        my ($self, $type) = @_;
        return 1 if $types{$type};
    }

    my %relationships => (
        # Articles has two relationships, one to comments, and
        # one to authors -- but authors actually has type 'people'
        articles => {
            comments => { type => 'comments', one_to_many => 1 },
            authors  => { type => 'people',   one_to_many => 0 },
        },
    );
    # Returns true if $type has a $rel_type relationships
    sub has_relationship {
        my ( $self, $type, $rel_name ) = @_;
        return 1 if exists $relationships{$type}
                 && exists $relationships{$type}{$rel_type};
    }

    # Returns true if $rel has a on
    sub has_one_to_many_relationship {
        my ( $self, $type, $rel_name ) = @_;
        return unless $self->has_relationship($type, $rel_type);
        return 1 if $relationships{$type}{$rel_type}{one_to_many};
    }

The next method we have to define is type_has_fields. We'll get a type and an arrayref of fields, and we'll return true if all all elements in the arrayref are attributes of type.

    my %columns = (
        articles => [qw/ id title body created updated /],
        people   => [qw/ id name age gender /],
        comments => [qw/ id body /],
    );
    sub type_has_fields {
        my ($self, $type, $has_fields) = @_;
        return unless $self->has_type($type);

        my %type_columns = map +($_=>1), @{ $columns{$type} };
        return if grep !exists $type_columns{$_}, @$has_fields;
        return 1;
    }

With those out of the way, we can get to the meat of the repository:

API method

All of these will receive at least these two arguments: type, the type of the request, and document, an instance of PONAPI::Builder::Document.

Note that by the time the arguments reach the API methods, some amount of validation will have already happened, like if the requested relationships exists.

retrieve_all( type => $type, %optional_arguments )

Our starting point:

    sub retrieve_all {
        my ($self, %args) = @_;
        my $type     = $args{type};
        my $document = $args{document};

        my $dbh   = $self->dbh;
        my $table = $self->quote($type);
        my $sth   = $dbh->prepare(qq{ SELECT * FROM $table });

        return unless $sth->execute;

        while ( my $row = $sth->fetchrow_hashref ) {
            my $id = delete $row->{id};

            # Add a new resource, identified by $type and $id
            my $resource = $doc->add_resource( type => $type, id => $id );
            # These will show up under the attributes key
            $resource->add_attribute( $_ => $row->{$_} ) for keys %{$row};
            # Add a links key, with pointer to ourselves
            $resource->add_self_link();
        }

        return;
    }

A call to $dao-retrive_all(type => 'articles', ...)> will then return something like this:

    {
        data => [
            {
                type => 'articles',
                id   => 123456,
                attributes => {
                    title   => 'This Or That',
                    body    => '...',
                    updated => '...',
                    created => '...',
                },
                links => {
                    self => '/articles/123456',
                },
            },
            ...
        ],
    }

To be fully API compliant, we should also return the relationships that a given resource has. To do this, we'll need a way of finding what our related resources are. For simplicity, let's assume that our data in SQL is in some junction tables:

    -- articles-to-authors, one to one
    CREATE TABLE rel_articles_people (
        id_articles   INTEGER     NOT NULL PRIMARY KEY,
        id_people     INTEGER     NOT NULL
    )

    -- articles-to-comments, one to many
    CREATE TABLE IF NOT EXISTS rel_articles_comments (
        id_articles   INTEGER     NOT NULL,
        id_comments   INTEGER     UNIQUE     NOT NULL
    )

Then, we can change %relationships to have that same information:

    my %relationships => (
        articles => {
            comments => {
                type          => 'comments',
                one_to_many   => 1,
                rel_table     => 'rel_articles_comments',
                id_column     => 'id_articles',
                rel_id_column => 'id_comments',
            },
            authors  => ...,
        },
        ...
    );

    sub _fetch_relationships {
        my ($self, %args) = @_;
        my ($type, $id)   = @args{qw/ type id /};

        my $type_relationships = $relationships{$type} || {};
        return unless %$type_relationships;

        my $dbh = $self->dbh;
        my %rels;
        foreach my $rel_type ( keys %$type_relationships ) {
            my $relationship_info = $type_relationships->{$rel_type};
            my $table             = $relationship_info->{rel_table};
            my $id_col            = $relationship_info->{id_column};
            my $rel_id_col        = $relationship_info->{rel_id_column};

            my $sth = $dbh->prepare(
                "SELECT $rel_id_col FROM $table WHERE $id_col = ?"
            );
            return unless $sth->execute($id);

            while ( my $row = $sth->fetchrow_hashref ) {
                push @{ $rels{$rel_type} ||= []}, $row->{$rel_id_col};
            }
        }

        return \%rels;
    }

    sub _add_relationships {
        my ($self, %args) = @_;
        my $resource = $args{resource};

        my $all_relationships = $self->_fetch_relationships(%args);
        foreach my $rel_type ( keys %$all_relationships )        {
            my $relationships = $all_relationships->{$rel_type} || [];
            next unless @$relationships;

            my $one_to_many = $self->has_one_to_many_relationship($type, $rel_type);
            foreach my $rel_id ( @$relationships ) {
                $resource->add_relationship( $rel_type, $rel_id, $one_to_many )
                    ->add_self_link
                    ->add_related_link;
            }
        }
    }

    sub retrieve_all {
        ...
        # Inside the while loop:
        $self->_add_relationships(
            %args,
            resource => $resource,
        );
        ...
    }

With this as our starting base, let's go over all the potential arguments we can receive, and see how we can implement them.

include

Spec.

This will contain an arrayref of relationship names, like one of these:

    [qw/ authors /]
    [qw/ authors comments /]

If present, our response needs to include a top-level included key with has full resources for every relationship of the types in the arrayref. For instance, if we get include = [qw/ comments /]>, and the requested resources have two comments, then our include will have those two, with the same information as if the user had manually called retrieve on each of them.

The responses should look something like this:

    {
        data     => [... same as before ...],
        included => [
            {
                type => 'comments'
                id   => 44,
                attributes => {
                    body  => 'I make terrible comments',
                },
                links => { self => '/comments/44' },
            }
            {
                type => 'comments'
                id   => 45,
                attributes => {
                    body  => 'I make even worse comments!',
                },
                links => { self => '/comments/45' },
            }
        ],
    }

In our repo:

    sub _add_included {
        my ( $self, $type, $ids, %args ) = @_;
        my ( $doc ) = @args{qw< document >};

        my $placeholders = join ", ", ('?') x @$ids;
        my $sql = "SELECT * FROM $type WHERE id IN ( $placeholders )";

        my $sth = $self->dbh->prepare($sql);
        $sql->execute(@$ids);

        while ( my $inc = $sth->fetchrow_hashref() ) {
            my $id = delete $inc->{id};
            $doc->add_included( type => $type, id => $id )
                ->add_attributes( %{$inc} )
                ->add_self_link;
        }
    }

    sub _add_relationships {
        ... # same as before
        # Inside the main for loop
        $self->_add_include(
            $rel_type, $relationships,
            %args,
        );
        ...
    }
fields

Spec.

This will arrive as a hashref looking something like this:

    type   => 'articles',
    fields => {
        articles => [qw/ title /],
    }

Here, the meaning is quite simple: only return the 'title' attribute of the resource:

    {
        type => 'articles',
        id   => 2,
        attributes => {
            title => "The great title!",
        },
        # No relationships requested!
        relationships => {},
    }

However, not only can we get attributes in fields, but also relationships. Consider a request with this:

    type   => 'articles',
    fields => {
        articles   => [qw/ title authors /],
    }

In this case, we'll want to return this:

    {
        type => 'articles',
        id   => 2,
        attributes => {
            title => "The great title!",
        },
        # No relationships requested!
        relationships => {
            authors => { type => 'people', id => 44 },
        },
    }

Moreso, fields can be combined with include:

    type    => 'articles',
    include => [qw/ authors /],
    fields  => {
        articles => [], # no attributes or relationships
        people   => [qw/ name /],
    },

And the response:

    data => [
        { type => "articles", id => 2, },
        { type => "articles", id => 3, },
    ],
    include => [
        {
            type => 'people', id => 44,
            attributes => { name => "Foo" },
        },
        {
            type => 'people', id => 46,
            attributes => { name => "Bar" },
        }
    ]

Sadly for the SQL example, this means we'll have to filter our relationships out of fields manually. On the bright side, PONAPI::DAO will have already validated that all of the requested fields are valid for the given type, so we can avoid those steps.

Let's tackle fetching only soem attributes first. So far, we have two SELECT statements, one in retrieve_all, and one in _add_included; since both will need to handle fields, let's add a function to abstract that complexity away:

    sub _columns_for_select {
        my ($self, %args) = @_;
        my $type    = $args{type};
        my $columns = $columns{$type};

        if ( my $fields = $args{fields}{$type} ) {
            my $type_relationships = $relationships{$type} || {};
            $columns = [
                'id',
                grep !exists $type_relationships->{$_}, @$fields
            ];
        }

        return $columns;
    }

Now, we could do something like this in retrieve_all and _add_included:

    ...
    my $table   = $self->quote($type);
    my $columns = $self->_columns_for_select(%args);

    $columns    = join ', ', map $dbh->quote($_), @$columns;
    my $sth     = $dbh->prepare(qq{ SELECT $columns FROM $table });
    ...

(At this point, you should be seriously looking into alternatives to composing your own SQL. For the implementation used for testing this module, we used SQL::Composer; we'll continue rolling our own SQL here, but please don't do that. For your sanity.)

We'll also want to modify our _fetch_relationships to handle fields requesting only certain relationships:

    ...
    foreach my $rel_type ( keys %$type_relationships ) {
        if ( $args{fields} && $args{fields}{$type} ) {
            next unless exists $args{fields}{$type}{$rel_type};
        }
    ...
page

Spec.

{json:api} doesn't specify much for page. The repository will get a hash, but the contents and format are entirely implementation dependent. For simplicity, let's take an offset-based approach, where page should look like this:

    page => {
        offset => 15000,
        limit  => 5000,
    }

One options is to implement this in terms of a LIMIT:

    ... # in retrieve_all
    # Make sure to validate $args{page} first! The DAO won't do it
    # for you!
    my %page        = %{ $args{page} || {} };
    $page{offset} //= 0;

    my ($limit, $offset) = @page{qw/limit offset/};

    $sql .= "\nLIMIT $offset, $limit" if $limit;
    ...

So far so good, but we may want to provide pagination links for the user to fetch the next batch of data.

    my ($self, %args) = @_;
    my ($page, $rows_fetched, $document) = @args{qw/page rows document/};

    my ($offset, $limit) = @{$page}{qw/offset limit/};

    my %current = %$page;
    my %first = ( %current, offset => 0, );
    my (%previous, %next);

    if ( ($offset - $limit) >= 0 ) {
        %previous = %current;
        $previous{offset} -= $current{limit};
    }

    if ( $rows_fetched >= $limit ) {
        %next = %current;
        $next{offset} += $limit;
    }

    $document->add_pagination_links(
        first => \%first,
        self  => \%current,
        prev  => \%previous,
        next  => \%next,
    );
sort

Spec.

Will arrive as an arrayref of attributes, relationship names, and relationship attributes:

    type => 'articles',
    # Just attributes
    sort => [qw/ created title /],

    type => 'articles',
    # attributes + relationship names
    sort => [qw/ created comments /],

    type => 'articles',
    # relationship attributes
    sort => [qw/ authors.name /],

Fields may start with a minus (-), which means they must be sorted in descending order:

    type => 'articles',
    # created DESC, title ASC
    sort => [qw/ -created title /],

For our SQL example, we'll only support sorting by first-level attributes, to keep the code simple:

    ...
    my $sort     = $args{sort} || [];
    my $sort_sql = join ", ", map {
        my ($desc, $col) = /\A(-?)(.+)\z/;
        $col = exists $columns{$type}{$col} ? $dbh->quote($col) : undef;
        $col
            ? ( $desc ? "$col DESC" : "$col ASC" )
            : ();
    } @$sort;

    $sql .= "\n ORDER BY $sort_sql" if $sort_sql;
    ...

Implementation note for SQL repositories: While not required, we recommend implementing sort so that sorting by id automagically uses whatever the underlaying column name is. This is to avoid clients needing to know specifics of the repo implementation to sort by the primary key:

    # Bad, client needs to know that id => id_articles
    $client->retrieve_all(
        type => 'articles',
        sort => [qw/article_id/],
    );

    # Good, client just sorts by id, repo handles changing that to id_articles
    $client->retrieve_all(
        type => 'articles',
        sort => [qw/id/],
    );
filter

Spec.

This is almost entirely up to the repo. You will receive a hashref, but the contents and how to handle them are entirely up to you.

To make implementing retrieve simpler, the example will implement a filter that allows for this:

    filter => {
        id  => [ 1, 2, 55 ], # id IN (1, 2, 55)
        age => 44,           # age = 44
    },

And in the code:

    ...
    my $filter = $args{filter} || {};
    my (@bind, @where);
    foreach my $col ( grep { exists $columns{$type}{$_} } keys %$filer ) {
        my $filter_values = $filter{$col};

        my $quoted = $dbh->quote($col);
        my $where;
        if ( ref $filter_values ) {
            my $placeholder_csv = join ', ', ('?') x @$filter_values;
            $where = "$quoted IN ( $placeholder_csv )";
        }
        else {
            $filter_values = [ $filter_values ];
            $where = "$quoted = ?";
        }

        push @where, "($where)";
        push @bind, @$filter_values;
    }
    my $where = join " AND ", @where;
    ...
    $sql .= "\n$where" if $where;
    ...
    $sth->execute(@bind);
    ...

Note that the spec does not define any way to express complex filters, like <= or even a simple OR; this may change in future versions of the spec, or extended in future versions of PONAPI.

retrieve( type => $type, id => $id, %optional_arguments )

Spec.

Similar to retrieve_all, but instead of returning multiple resources, it returns just one. Takes all the same arguments as retrieve_all, but page and sort are only relevant for included resources.

Since our <retrieve_all> example has a working filter, we can implement retrieve based on top of that:

    sub retrieve {
        my ($self, %args) = @_;
        my $type = $args{type};
        my $id   = delete $args{id};
        $args{filter}{$type} = $id;

        # This is missing page+sort of the included arguments
        return $self->retrieve_all(%args);
    }

retrieve_relationships( type => $type, id => $id, rel_type => $rel_type, %optional_arguments )

Spec.

For one-to-one relationships, this returns a single resource identifier:

    $dao->retrieve_relationships( type => 'articles, id => 1, rel_type => 'authors' );
    # returns
    data => { type => 'people', id => 6 }

For one-to-many, it returns a collection of resource identifiers:

    $dao->retrieve_relationships( type => 'articles, id => 1, rel_type => 'comments' );
    # returns
    data => [
        { type => 'comments', id => 45 },
        { type => 'comments', id => 46 },
    ]

retrieve_relationships may get a subset of the optional arguments for retrieve_all: filter, page, and sort.

For the client, retrieve_relationships serves primarily a shortcut to this:

    my $response = $client->retrieve(
        type     => 'articles',
        id       => 2,
        rel_type => 'comments',
        fields   => {
            articles => [qw/comments/],
            comments => [qw/id/],
        },
    )->{data}{relationships}{comments};

With the added functionality that they may page the results.

retrieve_by_relationship( type => $type, id => $id, rel_type => $rel_type, %optional_arguments )

Similar to retrieve_relationships, but returns full resources, rather than just resource identifiers.

delete(type => $type, id => $id)

Spec.

Removes a single resource.

    sub delete {
        my ($self, %args) = @_;
    
        my $table = $self->quote($type);
        my $sth   = $dbh->prepare(qq{ DELETE FROM $table WHERE id = ? });
    
        $sth->execute($args{id});
    }

It's up to the repo if it also wants to remove any relationships what the resource may have.

create(type => $type, data => $data, %optinal_arguments)

Spec.

Create a new resource of type $type, using the data provided. The only optional argument is id -- repos can choose to use client-provided IDs, if desired.

$data will be in this format:

    $data => {
        type          => $type,
        attributes    => { ... },
        relationships => {
            foo => { ... },          # one-to-one
            bar => [ { ... }, ... ], # one-to-many
        },
    }

update( type => $type, id => $id, data => { ... } )

Spec.

Updates the resource. data may have one or both of attributes and relationships:

Change the title, leave everything else alone: data => { type => 'articles', id => 2, attributes => { title => 'New title!', }, }

Update the one-to-one author relationship, leave everything else alone: data => { type => 'articles', id => 2, relationships => { authors => { type => 'people', id => 3 }, } }

Update the one-to-many comments relationship, clear the one-to-one author relationship, change the body of the article:

    data => {
        type => 'articles',
        id   => 2,
        attributes    => { body => "New and improved", },
        relationships => {
            authors  => undef,
            comments => [
                { type => 'comments', id => 99  },
                { type => 'comments', id => 100 },
            ],
        }
    }

Clear the one-to-many comments relationship:

    data => {
        type => 'articles',
        id   => 2,
        relationships => { comments => [] }
    }

update has a strict return value! It MUST return one of three constants imported by PONAPI::DAO::Constants:

PONAPI_UPDATED_NOTHING

The requested update did nothing. Amongst other reasons, this may be because the resource doesn't exist, or the changes were no-ops.

Spec-wise, this is used to distinguish between returning a 200/202 and returning a 204 or 404; see http://jsonapi.org/format/#crud-updating-relationship-responses-204 and http://jsonapi.org/format/#crud-updating-responses-404

Depending on the underlaying implementation, this may be hard to figure out, so implementations may chose to just return PONAPI_UPDATED_NORMAL instead.

(in DBD::mysql, you'll need to either connect to the database with mysql_client_found_rows, or parse $dbh-{mysql_info}> for Changed immediately after the UPDATE)

PONAPI_UPDATED_EXTENDED

The request asked us to update 2 rows, but for whatever reason, we updated more. As an example, this may be due to automatically adding the updated_at column on every update.

Depending on the server configuration, returning PONAPI_UPDATED_EXTENDED may trigger an extra retrieve on the updated resource, to ensure that the client has the most up to date data.

PONAPI_UPDATED_NORMAL

Everything went fine and we updated normally.

Code example:

    sub update {
        my ($self, %args) = @_;
        my ($type, $id, $data) = @args{qw/type id data/};

        my ($attributes, $relationships) = map $_||{}, @{$data}{qw/attributes relationships/};

        my $return_value = PONAPI_UPDATED_NORMAL;
        if ( %$attributes ) {
            my $dbh = $self->dbh;

            my ( @update, @values_for_update);
            while ( my ($column, $new_value) = each %$attributes ) {
               push @update, $dbh->quote($_) . " = ?";
               push @values_for_update, $new_value;
            }

            my $sql = "UPDATE $table SET "
                    . join( ', ', @update )
                    . 'VALUES ('
                    . join( ',', ('?') x @values_for_update )
                    . ')';

            my $sth = $dbh->prepare($sql);
            $sth->execute(@values_for_update);

            if ( !$sth->rows ) {
                # Woah there. Either the resource doesn't exist, or
                # the update did nothing
                return PONAPI_UPDATED_NOTHING;
            }
        }

        foreach my $rel_type ( keys %$relationships ) {
            # We'll get to this later
            $self->update_relationships(
                %args,
                rel_type => $rel_type,
                data     => $relationships->{$rel_type},
            );
        }
    }

Since we're not adding any extra columns, we can ignore PONAPI_UPDATED_EXTENDED.

delete_relationships( type => $type, id => $id, rel_type => $rel_type, data => [ { ... }, ... ] )

Spec.

Removes the resource(s) in data as from the one-to-many relationship pointed by $type and $rel_type.

Like update, delete_relationships has a strict return value.

    sub delete_relationships {
        my ( $self, %args ) = @_;
        my ( $type, $id, $rel_type, $data ) = @args{qw< type id rel_type data >};

        my $relationship_info = $type_relationships->{$rel_type};
        my $table             = $relationship_info->{rel_table};
        my $id_column         = $relationship_info->{id_column};
        my $rel_id_column     = $relationship_info->{rel_id_column};
        my $key_type          = $relationship_info->{type};

        my @all_values;
        foreach my $resource ( @$data ) {
            my $data_type = $resource->{type};

            # This is one of the few cases when we need to manually validate
            # that the data is correct -- this catches cases like this:
            # {
            #   rel_type => 'comments',
            #   data => [
            #       { type => comments => id => 5  }, # Good
            #       { type => people   => id => 19 }, # Bad, why people?
            #   ],
            # }
            if ( $data_type ne $key_type ) {
                PONAPI::DAO::Exception->throw(
                    message          => "Data has type `$data_type`, but we were expecting `$key_type`",
                    bad_request_data => 1,
                );
            }

            push @all_values, $resource->{id};
        }

        my $rows_modified = 0;
        my $sql = "DELETE FROM $table WHERE $id_column = ? AND $rel_id_column = ?";
        my $sth = $self->dbh->prepare($sql);
        foreach my $rel_id ( @all_values ) {
            $sth->execute($id, $rel_id);
            $rows_modified += $sth->rows;
        }

        return !$rows_modified
            ? PONAPI_UPDATED_NOTHING
            : PONAPI_UPDATED_NORMAL;
    }

create_relationships( type => $type, id => $id, rel_type => $rel_type, data => [ { ... }, ... ] )

Spec.

Adds the resource(s) in data as new members to the one-to-many relationship pointed by $type and $rel_type.

Like update, create_relationships has a strict return value.

update_relationships( type => $type, id => $id, rel_type => $rel_type, data => ... )

Spec.

Unlike the previous two methods, update_relationships handles both one-to-many and one-to-one relationships. For one-to-one, data will be either a hashref, or undef; for a one-to-many, it'll be an arrayref.

    data => undef,                           # clear the one-to-one
    data => { type => 'people', id => 781 }, # update the one-to-one
    data => [],                              # clear the one-to-many,
    data => [                                # update the one-to-many
        { type => 'comments', id => 415 },
        { type => 'comments', id => 416 },
    ]

Like update, update_relationships has a strict return value.

    sub _update_relationships {
        my ($self, %args) = @_;
        my ( $type, $id, $rel_type, $data ) = @args{qw< type id rel_type data >};

        my $relationship_info = $type_relationships->{$rel_type};
        my $rel_table         = $relationship_info->{rel_table};
        my $id_column         = $relationship_info->{id_column};
        my $rel_id_column     = $relationship_info->{rel_id_column};

        # Let's have an arrayref
        $data = $data
                ? ref($data) eq 'HASH' ? [ keys(%$data) ? $data : () ] : $data
                : [];

        # Let's start by clearing all relationships; this way
        # we can implement the SQL below without adding special cases
        # for ON DUPLICATE KEY UPDATE and sosuch.
        my $sql = "DELETE FROM $rel_table WHERE $id_column = ?";
        my $delete_sth = $self->dbh->prepare($sql);
        $delete_sth->execute($id);

        # And now, update the relationship by inserting into it
        my $sql = "INSERT INTO $rel_table ($id_column, $id_rel_column) VALUES (?, ?)";
        my $sth->prepare($sql);
        foreach my $insert ( @$data ) {
            $sth->execute( $id, $insert{id} );
        }

        return PONAPI_UPDATED_NORMAL;
    }

Throwing exceptions

It is recommended that you throw exceptions in the repo by using PONAPI::DAO::Exception:

    try {
        ...
    }
    catch {
        PONAPI::DAO::Exception->throw(
            message => "Something has gone horribly wrong"
        );
    }

See the documentation of PONAPI::DAO::Exception for more examples, including the different of more specific exceptions you can throw.

Responding with a particular status

Usually this is not needed, but it's possible to manually set the status of most responses by using $document-set_status($n)> in any API method.

Repos without relationships

This boils down to:

    sub has_relationship {}
    sub has_one_to_many_relationship {}

    # These four should never be reached if the two has_* methods
    # above return false
    sub retrieve_relationships {}
    sub retrieve_by_relationship {}
    sub create_relationships {}
    sub delete_relationships {}
    sub update_relationships {}

AUTHOR

Mickey Nasriachi <mickey AT cpan DOT org>, Stevan Little <stevan AT cpan DOT org>, Brian Fraser <hugmeir AT cpan DOT org>

COPYRIGHT AND LICENSE

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