I have a flash card app with the following object model:
has_many :cards
has_many :tags
card
belongs_to :card_set
habtm :tags
tag
belongs_to :card_set
habits :cards
I would like to include a card’s tags whenever a card is requested, and particularly when a card_set’s cards are requested.
What is the best way to do this? Is there any way to do this declaratively in the resource file (or elsewhere)?
The docs seem to point you towards using a ResourceSerializer and serialize_to_hash in the controller but trying this seemed a bit messy.
As far as I know, you have to declare the included entities in your query - I'd love to know if there's a way to make it the default for a given resource, though.
Incidentally, the solution i've come up with so far is:
class CardsController < JsonApiController
# JsonApiController includes JSONAPI::ActsAsResourceController
def get_related_resources
if (params[:source] == 'card_sets')
card_set = CardSet.find params[:card_set_id]
card_serializer = JSONAPI::ResourceSerializer.new(
CardResource, include: ['tags'],
fields: {
tags: [:name]
}
)
cards_json = card_serializer.serialize_to_hash(card_set.cards.map { |card| CardResource.new card})
render json: cards_json.to_json
else
super
end
end
end
Manageable but would be nice if were a simpler way.
Got hint from the tests here
@kagemusha
Firstly, it might be worth pointing out that according to the json-api spec 1.0, if a server supports custom includes, then it must not include resources that are not explicitly included in the request. Obviously you can do whatever you want, but you'll find it much better for the sanity of yourself and the people you work with if you stick to one or the other.
http://jsonapi.org/format/#fetching-includes
You can modify which resources are being included by modifying the @request object that is created by JR in the controller. This, importantly, is a different object that request, which is the default that is created by rack in the rails controller. @request is created by the #setup_request method in the controller, which runs before the actions as well as the before filters. You can override that method to do whatever you like, but I've found it's easier to just modify @request.include_directives in the actions you like before calling super. Check out this code. You could throw the bottom method in a shared parent controller if you wanted to use it throughout.
class CardSetsController < JSONAPI::ResourceController
def show
if params[:includes] == "cards"
set_include_overrides "cards.tags"
end
super
end
def set_include_overrides(directive_names=nil)
if directive_names
names = directive_names.split(',')
directives = JSONAPI::IncludeDirectives.new(names)
end
@request.include_directives = directives
end
end
The way your described the relationships does exactly make sense with you code example. If you want to include the tags of the cards use 'cards.tags', but if you want to include both the cards and tags of the card set use 'cards,tags'
Because adding default includes would not be in compliance with the spec I would rather not add this as a standard option (however I can see how this could be very useful, and if the spec changed would be an easy addition).
The simplest option might be to create a before filter and directly manipulate the params[:include] value before the request is setup. Because of the way the include directives are parsed you can repeat an include, so you wouldn't even need to worry about duplicates. For example:
class CardSetsController < JSONAPI::ResourceController
prepend_before_filter :ensure_card_tags
def ensure_card_tags
if params[:include] && params[:include].include?('cards')
params[:include] = params[:include] + ',cards.tags'
end
end
end
Prepend was the piece I was missing there.
@lgebhardt I'm happy to defer to your judgement when it comes to interpretation of the spec, but what do you make of this? Seems to me that it's saying default includes are valid only if an endpoint does not also support custom includes.
http://jsonapi.org/format/#fetching-includes
Inclusion of Related Resources
An endpoint MAY return resources related to the primary data by default.
An endpoint MAY also support an include request parameter to allow the client to customize which related resources should be returned.
If an endpoint does not support the include parameter, it must respond with 400 Bad Request to any requests that include it.
If an endpoint supports the include parameter and a client supplies it, the server MUST NOT include unrequested resource objects in the included section of the compound document.
@mmartinson is correct. Default includes are actually allowed by the spec. However, if any custom includes are requested via the include parameter, then only those custom includes must be returned. In other words, default and custom includes must not be mixed.
Sorry, I was misremembering the spec. However in the case of card_sets it wouldn't work because cards.tags would only be needed when cards is included (as per the original question). However specifying cards.tags as the default include would be acceptable as far as the spec is concerned.
@kagemusha are you happy to close this?
i was thinking might be useful for reference, but if not then yes i can
close it.
On Fri, Aug 28, 2015 at 5:20 PM, Mike Martinson [email protected]
wrote:
@kagemusha https://github.com/kagemusha are you happy to close this?
—
Reply to this email directly or view it on GitHub
https://github.com/cerebris/jsonapi-resources/issues/389#issuecomment-135890210
.
Why don't we leave it open for reference. I think an option for default includes would make sense, and we can refer to this issue.
Our use cases will require distinguishing between different actions as well. One of our use cases is to return default includes by default only on create, another is to only return default includes on show (different models).
For the create-only use case, the server creates related objects that the client needs to know about, and we want to avoid a second request.
For the show-only use case, we want to include details when you ask for a single resource, but not on index, to reduce the amount of data the client will need to parse.
Any suggestions on how to achieve this with the current release, and maybe some idea of where to begin to implement it as a feature of JR?
@thomassnielsen This is something I had started working on as well. I've got some code that I need to test some more, but i'll throw it up to see what you and @lgebhardt think of the API.
I am generally inclined to keep as much out of the controller as possible, since JR has been ambitious in tackling this Rails antipattern, but I think that it's the right place for this feature, since it would be configurable on a per-action basis. I was thinking of a syntax that resembles controller filters.
class PostsController < JSONAPI::ResourceController
setup_includes only: [:create, :update] do |resource|
resource.comments, force: true
end
end
Could also be a place to tackle default filtering/base record set for included resources. Thoughts?
That could work. What does the force: true do in this case?
For now I'm using the prepend_before_filter technique that @lgebhardt described above, and that works. I would of course prefer a built in method for this.
Default includes would also be useful for polymorphic relations. For example, if you have resource
class RecommendationResource < JSONAPI::Resource
has_one :target, polymorphic: true
end
and target can be of type Event, Book and some more, then the only way to include some relations that are specific to one type (location in Event) is to use default includes, as currently query like
/recommendations?include=target,target.location
is not valid (you get error "location is not a valid relationship of targets")
This is a very short implementation that is easy to customize per action:
def show
params[:include] = "cards.tags" if params[:include].nil?
super
end
Essentially you're just defaulting the include query parameter and then letting all the normal behavior occur. This still allows the user to opt out of the include with ?include=.
Another, similar approach using Rails filter callbacks.
class BooksController < BaseController
before_action only: :show do
force_include_resource "authors"
force_include_resource "publishers"
end
private
def force_include_resource(resource_name)
# Include the specified resource, even if the request didn't ask for it.
# See https://github.com/cerebris/jsonapi-resources/issues/389,
# https://jsonapi.org/format/#fetching-includes
params[:include] = (params[:include] || "").split(",")
.append(resource_name)
.uniq
.join(",")
end
end
Consider putting force_include_resource in BaseController; it's only here for illustration purposes.
If you want to make the included resources a default (overridable, not forced), you could do something like:
before_action only: :show do
default_include_resources "authors", "publishers"
end
def default_include_resources(*resource_names)
# Include the specified resources if no included resources are specified.
params[:include] ||= resource_names.join(",")
end
Most helpful comment
This is a very short implementation that is easy to customize per action:
Essentially you're just defaulting the
includequery parameter and then letting all the normal behavior occur. This still allows the user to opt out of the include with?include=.