From 44f7eb8f00333c4fb9d49b58b19745f70dfff6d6 Mon Sep 17 00:00:00 2001 From: Mike Francis Date: Tue, 3 Feb 2015 13:34:58 +0000 Subject: [PATCH 1/2] Added support for a join param --- lib/WebAPI/DBIC/Resource/HAL/Role/DBIC.pm | 20 +- .../DBIC/Resource/HAL/Role/ItemWritable.pm | 11 +- lib/WebAPI/DBIC/Resource/JSONAPI/Role/DBIC.pm | 16 +- .../Resource/JSONAPI/Role/ItemWritable.pm | 11 +- lib/WebAPI/DBIC/Resource/Role/DBICParams.pm | 23 +- lib/WebAPI/DBIC/Resource/Role/ItemWritable.pm | 5 - t/media-hal/41-join-req.exp | 647 ++++++++++++++++++ t/media-hal/41-join-req.t | 90 +++ t/media-jsonapi/41-join-req.exp | 498 ++++++++++++++ t/media-jsonapi/41-join-req.t | 86 +++ 10 files changed, 1368 insertions(+), 39 deletions(-) create mode 100644 t/media-hal/41-join-req.exp create mode 100644 t/media-hal/41-join-req.t create mode 100644 t/media-jsonapi/41-join-req.exp create mode 100644 t/media-jsonapi/41-join-req.t diff --git a/lib/WebAPI/DBIC/Resource/HAL/Role/DBIC.pm b/lib/WebAPI/DBIC/Resource/HAL/Role/DBIC.pm index c773e5d..3d9e85e 100644 --- a/lib/WebAPI/DBIC/Resource/HAL/Role/DBIC.pm +++ b/lib/WebAPI/DBIC/Resource/HAL/Role/DBIC.pm @@ -12,14 +12,12 @@ use JSON::MaybeXS qw(JSON); use Moo::Role; - requires 'get_url_for_item_relationship'; requires 'render_item_as_plain_hash'; requires 'path_for_item'; requires 'add_params_to_url'; requires 'prefetch'; - sub render_item_as_hal_hash { my ($self, $item) = @_; @@ -53,13 +51,19 @@ sub render_item_as_hal_hash { return $data; } - sub _render_prefetch { my ($self, $item, $data, $prefetch) = @_; while (my ($rel, $sub_rel) = each %{$prefetch}){ next if $rel eq 'self'; + # Prevent joins calling the DB for no cached + # relations in the join statement + if ($self->param('join')){ + next unless + defined $item->{related_resultsets}{$rel} && + defined $item->{related_resultsets}{$rel}->get_cache; + } my $subitem = $item->$rel(); # XXX perhaps render_item_as_hal_hash but requires cloned WM, eg without prefetch @@ -69,21 +73,18 @@ sub _render_prefetch { # See http://blog.stateless.co/post/13296666138/json-linking-with-hal if (not defined $subitem) { $data->{_embedded}{$rel} = undef; # show an explicit null from a prefetch - } - elsif ($subitem->isa('DBIx::Class::ResultSet')) { # one-to-many rel + } elsif ($subitem->isa('DBIx::Class::ResultSet')){ # one-to-many rel my $rel_set_resource = $self->web_machine_resource( set => $subitem, prefetch => ref $sub_rel eq 'ARRAY' ? $sub_rel : [$sub_rel], ); $data->{_embedded}{$rel} = $rel_set_resource->render_set_as_list_of_hal($subitem); - } - else { + } else { $data->{_embedded}{$rel} = $self->render_item_as_plain_hash($subitem); } } } - sub render_set_as_list_of_hal { my ($self, $set, $render_method) = @_; $render_method ||= 'render_item_as_hal_hash'; @@ -93,7 +94,6 @@ sub render_set_as_list_of_hal { return $set_data; } - sub render_set_as_hal { my ($self, $set) = @_; @@ -123,7 +123,6 @@ sub render_set_as_hal { return $data; } - sub _hal_page_links { my ($self, $set, $base, $page_items, $total_items) = @_; @@ -161,5 +160,4 @@ sub _hal_page_links { return @link_kvs; } - 1; diff --git a/lib/WebAPI/DBIC/Resource/HAL/Role/ItemWritable.pm b/lib/WebAPI/DBIC/Resource/HAL/Role/ItemWritable.pm index 3f8f97e..3351ec2 100644 --- a/lib/WebAPI/DBIC/Resource/HAL/Role/ItemWritable.pm +++ b/lib/WebAPI/DBIC/Resource/HAL/Role/ItemWritable.pm @@ -11,13 +11,9 @@ use Devel::Dwarn; use Moo::Role; - requires 'decode_json'; requires 'request'; -requires '_pre_update_resource_method'; - - around '_build_content_types_accepted' => sub { my $orig = shift; my $self = shift; @@ -29,14 +25,12 @@ around '_build_content_types_accepted' => sub { sub from_hal_json { my $self = shift; - $self->_pre_update_resource_method( "_do_update_embedded_resources_hal" ); my $data = $self->decode_json( $self->request->content ); $self->update_resource($data, is_put_replace => 0); return; } - -sub _do_update_embedded_resources_hal { +before '_do_update_resource' => sub { my ($self, $item, $hal, $result_class) = @_; my $links = delete $hal->{_links}; @@ -83,7 +77,6 @@ sub _do_update_embedded_resources_hal { } return; -} - +}; 1; diff --git a/lib/WebAPI/DBIC/Resource/JSONAPI/Role/DBIC.pm b/lib/WebAPI/DBIC/Resource/JSONAPI/Role/DBIC.pm index d9366e3..b480981 100644 --- a/lib/WebAPI/DBIC/Resource/JSONAPI/Role/DBIC.pm +++ b/lib/WebAPI/DBIC/Resource/JSONAPI/Role/DBIC.pm @@ -75,6 +75,14 @@ sub render_jsonapi_prefetch_rel { $item_edit_rel_hooks->{$relname} = sub { my ($jsonapi_obj, $row) = @_; + # Prevent joins calling the DB for no cached + # relations in the join statement + if ($self->param('join')){ + return unless + defined $row->{related_resultsets}{$relname} && + defined $row->{related_resultsets}{$relname}->get_cache; + } + my $subitem = $row->$relname(); my $compound_links_for_rel = $compound_links->{$rel_typename} ||= {}; @@ -118,7 +126,6 @@ sub render_jsonapi_response { # return top-level document hashref next if $self->param('distinct'); while (my ($relname, $sub_rel) = each %{$prefetch}){ - #warn "prefetch $prefetch - $relname, $sub_rel"; $self->render_jsonapi_prefetch_rel($set, $relname, $sub_rel, $top_links, $compound_links, $item_edit_rel_hooks); } } @@ -180,6 +187,13 @@ sub _render_prefetch_jsonapi { while (my ($rel, $sub_rel) = each %{$prefetch}){ next if $rel eq 'self'; + # Prevent joins calling the DB for no cached + # relations in the join statement + if ($self->param('join')){ + next unless + defined $item->{related_resultsets}{$rel} && + defined $item->{related_resultsets}{$rel}->get_cache; + } my $subitem = $item->$rel(); if (not defined $subitem) { diff --git a/lib/WebAPI/DBIC/Resource/JSONAPI/Role/ItemWritable.pm b/lib/WebAPI/DBIC/Resource/JSONAPI/Role/ItemWritable.pm index 93d8533..d77613c 100644 --- a/lib/WebAPI/DBIC/Resource/JSONAPI/Role/ItemWritable.pm +++ b/lib/WebAPI/DBIC/Resource/JSONAPI/Role/ItemWritable.pm @@ -11,12 +11,9 @@ use Devel::Dwarn; use Moo::Role; - requires 'decode_json'; requires 'request'; -requires '_pre_update_resource_method'; - around '_build_content_types_accepted' => sub { my $orig = shift; @@ -26,17 +23,14 @@ around '_build_content_types_accepted' => sub { return $types; }; - sub from_jsonapi_json { my $self = shift; - $self->_pre_update_resource_method( "_do_update_embedded_resources_jsonapi" ); my $data = $self->decode_json( $self->request->content ); $self->update_resource($data, is_put_replace => 0); return; } - -sub _do_update_embedded_resources_jsonapi { +before '_do_update_resource' => sub { my ($self, $item, $jsonapi, $result_class) = @_; my $links = delete $jsonapi->{_links}; @@ -83,7 +77,6 @@ sub _do_update_embedded_resources_jsonapi { } return; -} - +}; 1; diff --git a/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm b/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm index ecaa12e..c4f30f6 100644 --- a/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm +++ b/lib/WebAPI/DBIC/Resource/Role/DBICParams.pm @@ -124,6 +124,11 @@ sub _handle_search_criteria_param { return; } +sub _handle_join_param { + my ($self) = shift; + $self->_handle_prefetch_param(@_); +} + sub _handle_prefetch_param { my ($self, $value) = @_; @@ -143,10 +148,20 @@ sub _handle_prefetch_param { $self->prefetch( $prefetch ); # include self, even if deleted below $prefetch = [grep { !defined $_->{self}} @$prefetch]; - my $prefetch_or_join = $self->param('fields') ? 'join' : 'prefetch'; - Dwarn { $prefetch_or_join => $prefetch } if $ENV{WEBAPI_DBIC_DEBUG}; - $self->set( $self->set->search_rs(undef, { $prefetch_or_join => $prefetch })) - if scalar @$prefetch; + my $join_args; + if ($self->param('fields') || $self->param('join')){ + $join_args = { + join => $prefetch, + collapse => 1, + }; + } else { + $join_args = { + prefetch => $prefetch, + }; + } + + Dwarn $join_args if $ENV{WEBAPI_DBIC_DEBUG}; + $self->set( $self->set->search_rs(undef, $join_args)) if scalar @$prefetch; return; } diff --git a/lib/WebAPI/DBIC/Resource/Role/ItemWritable.pm b/lib/WebAPI/DBIC/Resource/Role/ItemWritable.pm index 02f6be6..be6e842 100644 --- a/lib/WebAPI/DBIC/Resource/Role/ItemWritable.pm +++ b/lib/WebAPI/DBIC/Resource/Role/ItemWritable.pm @@ -69,11 +69,6 @@ sub delete_resource { return $_[0]->item->delete } sub _do_update_resource { my ($self, $item, $hal, $result_class) = @_; - # provide a hook for richer behaviour, eg HAL - my $_pre_update_resource_method = $self->_pre_update_resource_method; - $self->$_pre_update_resource_method($item, $hal, $result_class) - if $_pre_update_resource_method; - # By default the DBIx::Class::Row update() call below will only update the # columns where %$hal contains different values to the ones in $item # This is usually a useful optimization but not always. So we provide diff --git a/t/media-hal/41-join-req.exp b/t/media-hal/41-join-req.exp new file mode 100644 index 0000000..cd38876 --- /dev/null +++ b/t/media-hal/41-join-req.exp @@ -0,0 +1,647 @@ +=== join on an item using two belongs_to relationships +Request: +GET /cd/1?join=artist,genre +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "year" : "1999" +} + +=== join on a set using two belongs_to relationships +Request: +GET /cd?rows=2&page=1&join=artist,genre +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "cd" : [ + { + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "year" : "1999" + }, + { + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=2" + }, + "genre" : { + "href" : "/genre/2" + }, + "self" : { + "href" : "/cd/2" + }, + "tracks" : { + "href" : "/track?me.cd=2" + } + }, + "artist" : 1, + "cdid" : 2, + "genreid" : 2, + "single_track" : null, + "title" : "Forkful of bees", + "year" : "2001" + } + ] + }, + "_links" : { + "next" : { + "href" : "/cd?rows=2&page=2" + }, + "self" : { + "href" : "/cd?rows=2&page=1", + "title" : "TestSchema::Result::CD" + } + } +} + +=== filter on joined relation field +Request: +GET /cd?join=artist&artist.name=Random+Boy+Band +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "cd" : [ + { + "_links" : { + "artist" : { + "href" : "/artist/2" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=4" + }, + "genre" : { + "href" : "/genre/3" + }, + "self" : { + "href" : "/cd/4" + }, + "tracks" : { + "href" : "/track?me.cd=4" + } + }, + "artist" : 2, + "cdid" : 4, + "genreid" : 3, + "single_track" : null, + "title" : "Generic Manufactured Singles", + "year" : "2001" + } + ] + }, + "_links" : { + "self" : { + "href" : "/cd?rows=30&page=1", + "title" : "TestSchema::Result::CD" + } + } +} + +=== filter on join with JSON +Request: +GET /cd?join=artist PARAMS: artist.name~json=>{"like"=>"%Band"} +GET /cd?join=artist&artist.name~json=%7B%22like%22%3A%22%25Band%22%7D +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "cd" : [ + { + "_links" : { + "artist" : { + "href" : "/artist/2" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=4" + }, + "genre" : { + "href" : "/genre/3" + }, + "self" : { + "href" : "/cd/4" + }, + "tracks" : { + "href" : "/track?me.cd=4" + } + }, + "artist" : 2, + "cdid" : 4, + "genreid" : 3, + "single_track" : null, + "title" : "Generic Manufactured Singles", + "year" : "2001" + } + ] + }, + "_links" : { + "self" : { + "href" : "/cd?rows=30&page=1", + "title" : "TestSchema::Result::CD" + } + } +} + +=== multi type relation (has_many) in join on item +Request: +GET /artist/1?join=cds&order=cds.cdid +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_links" : { + "cds" : { + "href" : "/cd?me.artist=1" + }, + "cds_cref_cond" : { + "href" : "/cd/1" + }, + "self" : { + "href" : "/artist/1" + } + }, + "artistid" : 1, + "charfield" : null, + "name" : "Caterwauler McCrae", + "rank" : 13 +} + +=== multi type relation (has_many) in join on set +Request: +GET /artist?join=cds&order=me.artistid,cds.cdid&rows=2 +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "artist" : [ + { + "_links" : { + "cds" : { + "href" : "/cd?me.artist=1" + }, + "cds_cref_cond" : { + "href" : "/cd/1" + }, + "self" : { + "href" : "/artist/1" + } + }, + "artistid" : 1, + "charfield" : null, + "name" : "Caterwauler McCrae", + "rank" : 13 + }, + { + "_links" : { + "cds" : { + "href" : "/cd?me.artist=2" + }, + "cds_cref_cond" : { + "href" : "/cd/2" + }, + "self" : { + "href" : "/artist/2" + } + }, + "artistid" : 2, + "charfield" : null, + "name" : "Random Boy Band", + "rank" : 13 + } + ] + }, + "_links" : { + "next" : { + "href" : "/artist?rows=2&page=2" + }, + "self" : { + "href" : "/artist?rows=2&page=1", + "title" : "TestSchema::Result::Artist" + } + } +} + +=== multi type relation in join on item (many_to_many via JSON) ArrayRef Syntax +Request: +GET /cd/1?sort=cd_to_producer.producer PARAMS: join~json=>[{"cd_to_producer"=>"producer"}] +GET /cd/1?sort=cd_to_producer.producer&join~json=%5B%7B%22cd_to_producer%22%3A%22producer%22%7D%5D +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "year" : "1999" +} + +=== multi type relation in join on item (many_to_many via JSON) HashRef Syntax +Request: +GET /cd/1?sort=cd_to_producer.producer PARAMS: join~json=>{"cd_to_producer"=>"producer"} +GET /cd/1?sort=cd_to_producer.producer&join~json=%7B%22cd_to_producer%22%3A%22producer%22%7D +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "year" : "1999" +} + +=== filter on nested join +Request: +GET /artist?rows=2&producer.name=Matt+S+Trout PARAMS: join~json=>{"cds"=>{"cd_to_producer"=>"producer"}} cds.year~json=>{">","0996"} +GET /artist?rows=2&producer.name=Matt+S+Trout&join~json=%7B%22cds%22%3A%7B%22cd_to_producer%22%3A%22producer%22%7D%7D&cds.year~json=%7B%22%3E%22%3A%220996%22%7D +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "artist" : [ + { + "_links" : { + "cds" : { + "href" : "/cd?me.artist=1" + }, + "cds_cref_cond" : { + "href" : "/cd/1" + }, + "self" : { + "href" : "/artist/1" + } + }, + "artistid" : 1, + "charfield" : null, + "name" : "Caterwauler McCrae", + "rank" : 13 + } + ] + }, + "_links" : { + "self" : { + "href" : "/artist?rows=2&page=1", + "title" : "TestSchema::Result::Artist" + } + } +} + +=== join with query on ambiguous field +Request: +GET /cd/?me.artist=1&join=artist +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "cd" : [ + { + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "year" : "1999" + }, + { + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=2" + }, + "genre" : { + "href" : "/genre/2" + }, + "self" : { + "href" : "/cd/2" + }, + "tracks" : { + "href" : "/track?me.cd=2" + } + }, + "artist" : 1, + "cdid" : 2, + "genreid" : 2, + "single_track" : null, + "title" : "Forkful of bees", + "year" : "2001" + }, + { + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=3" + }, + "genre" : { + "href" : "/genre/2" + }, + "self" : { + "href" : "/cd/3" + }, + "tracks" : { + "href" : "/track?me.cd=3" + } + }, + "artist" : 1, + "cdid" : 3, + "genreid" : 2, + "single_track" : null, + "title" : "Caterwaulin' Blues", + "year" : "1997" + } + ] + }, + "_links" : { + "self" : { + "href" : "/cd?rows=30&me.artist=1&page=1", + "title" : "TestSchema::Result::CD" + } + } +} + +=== join on invalid name +Request: +GET /cd/1?join=nonesuch +Accept: application/hal+json,application/json +Response: +400 Bad Request +Content-type: application/json +{ + "errors" : [ + { + "_meta" : { + "relationship" : null, + "relationships" : [ + "artist", + "cd_to_producer", + "existing_single_track", + "genre", + "single_track", + "tracks" + ] + }, + "nonesuch" : "no relationship with that name\n" + } + ] +} + +=== join on set with partial response of joined items +Request: +GET /cd?rows=2&page=1&join=artist,genre&fields=cdid,artist,genreid,genre.genreid,artist.artistid +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "cd" : [ + { + "_embedded" : { + "artist" : { + "artistid" : 1 + }, + "genre" : { + "genreid" : 1 + } + }, + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1 + }, + { + "_embedded" : { + "artist" : { + "artistid" : 1 + }, + "genre" : { + "genreid" : 2 + } + }, + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=2" + }, + "genre" : { + "href" : "/genre/2" + }, + "self" : { + "href" : "/cd/2" + }, + "tracks" : { + "href" : "/track?me.cd=2" + } + }, + "artist" : 1, + "cdid" : 2, + "genreid" : 2 + } + ] + }, + "_links" : { + "next" : { + "href" : "/cd?rows=2&page=2" + }, + "self" : { + "href" : "/cd?rows=2&page=1", + "title" : "TestSchema::Result::CD" + } + } +} + +=== join on item with partial response of joined item +Request: +GET /cd/1?join=artist,genre&fields=cdid,artist,genreid,artist.artistid,genre.genreid +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_embedded" : { + "artist" : { + "artistid" : 1 + }, + "genre" : { + "genreid" : 1 + } + }, + "_links" : { + "artist" : { + "href" : "/artist/1" + }, + "cd_to_producer" : { + "href" : "/cd_to_producer?me.cd=1" + }, + "genre" : { + "href" : "/genre/1" + }, + "self" : { + "href" : "/cd/1" + }, + "tracks" : { + "href" : "/track?me.cd=1" + } + }, + "artist" : 1, + "cdid" : 1, + "genreid" : 1 +} + +=== join on item with id primary key #28 +Request: +GET /country/1?join=cities +Accept: application/hal+json,application/json +Response: +200 OK +Content-type: application/hal+json +{ + "_links" : { + "cities" : { + "href" : "/city?me.country_id=1" + }, + "self" : { + "href" : "/country/1" + } + }, + "id" : 1, + "name" : "England" +} + diff --git a/t/media-hal/41-join-req.t b/t/media-hal/41-join-req.t new file mode 100644 index 0000000..c162dcf --- /dev/null +++ b/t/media-hal/41-join-req.t @@ -0,0 +1,90 @@ +#!/usr/bin/env perl + +# TODO: this ought to be split up, eg testing requests on Item vs Set resources + + +use lib "t/lib"; +use TestKit; + +note $INC{"Cpanel/JSON/XS.pm"}; # temp XXX + +fixtures_ok [qw/basic/]; + +subtest "===== join =====" => sub { + my ($self) = @_; + + my $app = TestWebApp->new({ + schema => Schema, + })->to_psgi_app; + + run_request_spec_tests($app, \*DATA); + +}; + +done_testing(); + +__DATA__ +Config: +Accept: application/hal+json,application/json + +Name: join on an item using two belongs_to relationships +GET /cd/1?join=artist,genre + +Name: join on a set using two belongs_to relationships +GET /cd?rows=2&page=1&join=artist,genre + +Name: filter on joined relation field +# Only handle filter of the SET based on the join. DBIC won't allow filtering of the join on an ITEM +# as the WHERE clause is added to the whole select statement. +# Should only return CDs whose artist is Caterwauler McCrae +GET /cd?join=artist&artist.name=Random+Boy+Band + +Name: filter on join with JSON +GET /cd?join=artist PARAMS: artist.name~json=>{"like"=>"%Band"} + +Name: multi type relation (has_many) in join on item +# Return artist 1 and all cds. Ordered to ensure test stability. +# Artist->search({artistid => 1}, {join => 'cds'}) +GET /artist/1?join=cds&order=cds.cdid + +Name: multi type relation (has_many) in join on set +# Return all artists and all cds +# Artist->search({}, {join => 'cds'}) +GET /artist?join=cds&order=me.artistid,cds.cdid&rows=2 + +Name: multi type relation in join on item (many_to_many via JSON) ArrayRef Syntax +# Return all cds and all producers +# cd->search({}, {join => {cd_to_producers => 'producer'}) +# many_to_many relationships are not true db relationships. As such you can't use a many_to_many +# in a join but must traverse the join. +# Use sort to ensure test stability +GET /cd/1?sort=cd_to_producer.producer PARAMS: join~json=>[{"cd_to_producer"=>"producer"}] + +Name: multi type relation in join on item (many_to_many via JSON) HashRef Syntax +# Return all cds and all producers +# cd->search({}, {join => {cd_to_producers => 'producer'}) +# many_to_many relationships are not true db relationships. As such you can't use a many_to_many +# in a join but must traverse the join. +# Use sort to ensure test stability +GET /cd/1?sort=cd_to_producer.producer PARAMS: join~json=>{"cd_to_producer"=>"producer"} + +Name: filter on nested join +# Return all artists who have a CD created after 1997 who's producer is Matt S Trout +# Artist->search({cds.year => ['>', '1997'], producers.name => 'Matt S Trout'}, {join => [{cds => producers}]}) +GET /artist?rows=2&producer.name=Matt+S+Trout PARAMS: join~json=>{"cds"=>{"cd_to_producer"=>"producer"}} cds.year~json=>{">","0996"} + +Name: join with query on ambiguous field +# just check that a 'artist is ambiguous' error isn't generated +GET /cd/?me.artist=1&join=artist + +Name: join on invalid name +GET /cd/1?join=nonesuch + +Name: join on set with partial response of joined items +GET /cd?rows=2&page=1&join=artist,genre&fields=cdid,artist,genreid,genre.genreid,artist.artistid + +Name: join on item with partial response of joined item +GET /cd/1?join=artist,genre&fields=cdid,artist,genreid,artist.artistid,genre.genreid + +Name: join on item with id primary key #28 +GET /country/1?join=cities diff --git a/t/media-jsonapi/41-join-req.exp b/t/media-jsonapi/41-join-req.exp new file mode 100644 index 0000000..4dcda22 --- /dev/null +++ b/t/media-jsonapi/41-join-req.exp @@ -0,0 +1,498 @@ +=== join on an item using two belongs_to relationships +Request: +GET /cd/1?join=artist,genre +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "type" : "cd", + "year" : "1999" + } + ], + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + }, + "cd.genre" : { + "href" : "/genre/{genre.genreid}", + "type" : "genre" + } + } +} + +=== join on a set using two belongs_to relationships +Request: +GET /cd?rows=2&page=1&join=artist,genre +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "type" : "cd", + "year" : "1999" + }, + { + "artist" : 1, + "cdid" : 2, + "genreid" : 2, + "href" : "/cd/2", + "id" : 2, + "single_track" : null, + "title" : "Forkful of bees", + "type" : "cd", + "year" : "2001" + } + ], + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + }, + "cd.genre" : { + "href" : "/genre/{genre.genreid}", + "type" : "genre" + } + } +} + +=== filter on joined relation field +Request: +GET /cd?join=artist&artist.name=Random+Boy+Band +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 2, + "cdid" : 4, + "genreid" : 3, + "href" : "/cd/4", + "id" : 4, + "single_track" : null, + "title" : "Generic Manufactured Singles", + "type" : "cd", + "year" : "2001" + } + ], + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + } + } +} + +=== filter on join with JSON +Request: +GET /cd?join=artist PARAMS: artist.name~json=>{"like"=>"%Band"} +GET /cd?join=artist&artist.name~json=%7B%22like%22%3A%22%25Band%22%7D +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 2, + "cdid" : 4, + "genreid" : 3, + "href" : "/cd/4", + "id" : 4, + "single_track" : null, + "title" : "Generic Manufactured Singles", + "type" : "cd", + "year" : "2001" + } + ], + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + } + } +} + +=== multi type relation (has_many) in join on item +Request: +GET /artist/1?join=cds&order=cds.cdid +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "artist" : [ + { + "artistid" : 1, + "charfield" : null, + "href" : "/artist/1", + "id" : 1, + "name" : "Caterwauler McCrae", + "rank" : 13, + "type" : "artist" + } + ], + "links" : { + "artist.cds" : { + "href" : "/cd?me.artist={cds.artistid}", + "type" : "cd" + } + } +} + +=== multi type relation (has_many) in join on set +Request: +GET /artist?join=cds&order=me.artistid,cds.cdid&rows=2 +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "artist" : [ + { + "artistid" : 1, + "charfield" : null, + "href" : "/artist/1", + "id" : 1, + "name" : "Caterwauler McCrae", + "rank" : 13, + "type" : "artist" + }, + { + "artistid" : 2, + "charfield" : null, + "href" : "/artist/2", + "id" : 2, + "name" : "Random Boy Band", + "rank" : 13, + "type" : "artist" + } + ], + "links" : { + "artist.cds" : { + "href" : "/cd?me.artist={cds.artistid}", + "type" : "cd" + } + } +} + +=== multi type relation in join on item (many_to_many via JSON) ArrayRef Syntax +Request: +GET /cd/1 PARAMS: join~json=>[{"cd_to_producer"=>"producer"}] +GET /cd/1?join~json=%5B%7B%22cd_to_producer%22%3A%22producer%22%7D%5D +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "type" : "cd", + "year" : "1999" + } + ] +} + +=== multi type relation in join on item (many_to_many via JSON) HashRef Syntax +Request: +GET /cd/1 PARAMS: join~json=>{"cd_to_producer"=>"producer"} +GET /cd/1?join~json=%7B%22cd_to_producer%22%3A%22producer%22%7D +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "type" : "cd", + "year" : "1999" + } + ] +} + +=== filter on nested join +Request: +GET /artist?rows=2&producer.name=Matt+S+Trout PARAMS: join~json=>{"cds"=>{"cd_to_producer"=>"producer"}} cds.year~json=>{">","0996"} +GET /artist?rows=2&producer.name=Matt+S+Trout&join~json=%7B%22cds%22%3A%7B%22cd_to_producer%22%3A%22producer%22%7D%7D&cds.year~json=%7B%22%3E%22%3A%220996%22%7D +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "artist" : [ + { + "artistid" : 1, + "charfield" : null, + "href" : "/artist/1", + "id" : 1, + "name" : "Caterwauler McCrae", + "rank" : 13, + "type" : "artist" + } + ], + "links" : { + "artist.cds" : { + "href" : "/cd?me.artist={cds.artistid}", + "type" : "cd" + } + } +} + +=== join with query on ambiguous field +Request: +GET /cd/?me.artist=1&join=artist +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "single_track" : null, + "title" : "Spoonful of bees", + "type" : "cd", + "year" : "1999" + }, + { + "artist" : 1, + "cdid" : 2, + "genreid" : 2, + "href" : "/cd/2", + "id" : 2, + "single_track" : null, + "title" : "Forkful of bees", + "type" : "cd", + "year" : "2001" + }, + { + "artist" : 1, + "cdid" : 3, + "genreid" : 2, + "href" : "/cd/3", + "id" : 3, + "single_track" : null, + "title" : "Caterwaulin' Blues", + "type" : "cd", + "year" : "1997" + } + ], + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + } + } +} + +=== join on invalid name +Request: +GET /cd/1?join=nonesuch +Accept: application/vnd.api+json +Response: +400 Bad Request +Content-type: application/json +{ + "errors" : [ + { + "_meta" : { + "relationship" : null, + "relationships" : [ + "artist", + "cd_to_producer", + "existing_single_track", + "genre", + "single_track", + "tracks" + ] + }, + "nonesuch" : "no relationship with that name\n" + } + ] +} + +=== join on set with partial response of joined items +Request: +GET /cd?rows=2&page=1&join=artist,genre&fields=cdid,artist,genreid,genre.genreid,artist.artistid +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "links" : { + "artist" : 1, + "genre" : 1 + }, + "type" : "cd" + }, + { + "artist" : 1, + "cdid" : 2, + "genreid" : 2, + "href" : "/cd/2", + "id" : 2, + "links" : { + "artist" : 1, + "genre" : 2 + }, + "type" : "cd" + } + ], + "linked" : { + "artist" : [ + { + "artistid" : 1, + "href" : "/artist/1", + "id" : 1, + "type" : "artist" + } + ], + "genre" : [ + { + "genreid" : 1, + "href" : "/genre/1", + "id" : 1, + "type" : "genre" + }, + { + "genreid" : 2, + "href" : "/genre/2", + "id" : 2, + "type" : "genre" + } + ] + }, + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + }, + "cd.genre" : { + "href" : "/genre/{genre.genreid}", + "type" : "genre" + } + } +} + +=== join on item with partial response of joined item +Request: +GET /cd/1?join=artist,genre&fields=cdid,artist,genreid,artist.artistid,genre.genreid +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "cd" : [ + { + "artist" : 1, + "cdid" : 1, + "genreid" : 1, + "href" : "/cd/1", + "id" : 1, + "links" : { + "artist" : 1, + "genre" : 1 + }, + "type" : "cd" + } + ], + "linked" : { + "artist" : [ + { + "artistid" : 1, + "href" : "/artist/1", + "id" : 1, + "type" : "artist" + } + ], + "genre" : [ + { + "genreid" : 1, + "href" : "/genre/1", + "id" : 1, + "type" : "genre" + } + ] + }, + "links" : { + "cd.artist" : { + "href" : "/artist/{artist.artist}", + "type" : "artist" + }, + "cd.genre" : { + "href" : "/genre/{genre.genreid}", + "type" : "genre" + } + } +} + +=== join on item with id primary key #28 +Request: +GET /country/1?join=cities +Accept: application/vnd.api+json +Response: +200 OK +Content-type: application/vnd.api+json +{ + "country" : [ + { + "href" : "/country/1", + "id" : 1, + "name" : "England", + "type" : "country" + } + ], + "links" : { + "country.cities" : { + "href" : "/city?me.country_id={cities.id}", + "type" : "city" + } + } +} + diff --git a/t/media-jsonapi/41-join-req.t b/t/media-jsonapi/41-join-req.t new file mode 100644 index 0000000..c7e8723 --- /dev/null +++ b/t/media-jsonapi/41-join-req.t @@ -0,0 +1,86 @@ +#!/usr/bin/env perl + +# TODO: this ought to be split up, eg testing requests on Item vs Set resources + + +use lib "t/lib"; +use TestKit; + +fixtures_ok [qw/basic/]; + +subtest "===== join =====" => sub { + my ($self) = @_; + + my $app = TestWebApp->new({ + schema => Schema, + })->to_psgi_app; + + run_request_spec_tests($app, \*DATA); + +}; + +done_testing(); + +__DATA__ +Config: +Accept: application/vnd.api+json + +Name: join on an item using two belongs_to relationships +GET /cd/1?join=artist,genre + +Name: join on a set using two belongs_to relationships +GET /cd?rows=2&page=1&join=artist,genre + +Name: filter on joined relation field +# Only handle filter of the SET based on the join. DBIC won't allow filtering of the join on an ITEM +# as the WHERE clause is added to the whole select statement. +# Should only return CDs whose artist is Caterwauler McCrae +GET /cd?join=artist&artist.name=Random+Boy+Band + +Name: filter on join with JSON +GET /cd?join=artist PARAMS: artist.name~json=>{"like"=>"%Band"} + +Name: multi type relation (has_many) in join on item +# Return artist 1 and all cds. Ordered to ensure test stability. +# Artist->search({artistid => 1}, {join => 'cds'}) +GET /artist/1?join=cds&order=cds.cdid + +Name: multi type relation (has_many) in join on set +# Return all artists and all cds +# Artist->search({}, {join => 'cds'}) +GET /artist?join=cds&order=me.artistid,cds.cdid&rows=2 + +Name: multi type relation in join on item (many_to_many via JSON) ArrayRef Syntax +# Return all cds and all producers +# cd->search({}, {join => [{cd_to_producers => 'producer'}]) +# many_to_many relationships are not true db relationships. As such you can't use a many_to_many +# in a join but must traverse the join. +GET /cd/1 PARAMS: join~json=>[{"cd_to_producer"=>"producer"}] + +Name: multi type relation in join on item (many_to_many via JSON) HashRef Syntax +# Return all cds and all producers +# cd->search({}, {join => [{cd_to_producers => 'producer'}]) +# many_to_many relationships are not true db relationships. As such you can't use a many_to_many +# in a join but must traverse the join. +GET /cd/1 PARAMS: join~json=>{"cd_to_producer"=>"producer"} + +Name: filter on nested join +# Return all artists who have a CD created after 1997 who's producer is Matt S Trout +# Artist->search({cds.year => ['>', '1997'], producers.name => 'Matt S Trout'}, {join => [{cds => producers}]}) +GET /artist?rows=2&producer.name=Matt+S+Trout PARAMS: join~json=>{"cds"=>{"cd_to_producer"=>"producer"}} cds.year~json=>{">","0996"} + +Name: join with query on ambiguous field +# just check that a 'artist is ambiguous' error isn't generated +GET /cd/?me.artist=1&join=artist + +Name: join on invalid name +GET /cd/1?join=nonesuch + +Name: join on set with partial response of joined items +GET /cd?rows=2&page=1&join=artist,genre&fields=cdid,artist,genreid,genre.genreid,artist.artistid + +Name: join on item with partial response of joined item +GET /cd/1?join=artist,genre&fields=cdid,artist,genreid,artist.artistid,genre.genreid + +Name: join on item with id primary key #28 +GET /country/1?join=cities From 396b9b8bf0f46bfc13657f1c666ac2299183b039 Mon Sep 17 00:00:00 2001 From: Mike Francis Date: Mon, 9 Feb 2015 09:08:16 +0000 Subject: [PATCH 2/2] #34: Added documentation for join param --- lib/WebAPI/DBIC.pm | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/WebAPI/DBIC.pm b/lib/WebAPI/DBIC.pm index de65dbb..bcefb56 100644 --- a/lib/WebAPI/DBIC.pm +++ b/lib/WebAPI/DBIC.pm @@ -480,6 +480,13 @@ Also see L. =head2 GET Item - Optional Parameters +=head3 join + +The Join parameter behaves in the same way as the prefetch parameter excepting in one important +fashion; use of the join command will only return the primary resource or the specific fields +requested. It should primarily be used for filtering or sorting the primary resource on a +secondary joined resource. see L for information on the format of the join command. + =head3 prefetch Prefetch is a mechanism in DBIx::Class by which related resultsets can be returned