Resources
Resources are where you define how to query and persist a given Model
.
We'll wire-up the JSONAPI contract for you, so you can customize hooks rather
than spend your time parsing query parameters.
To the right, you'll see potential overrides of default behavior. Our defaults are based on ActiveRecord
and Rails conventions - so customizations like this are often not required.
If you find yourself writing the same customizations over and over again, DRY them up by writing an Adapter.
See also:
class PostResource < ApplicationResource
# Use default logic (specified in adapter)
# when no block passed
allow_filter :active
allow_filter :title_prefix do |scope, value|
# Example:
# scope.where(["title LIKE ?", "#{value}%"])
end
sort do |scope, att, dir|
# Example:
# scope.order(att => dir)
end
paginate do |scope, current_page, per_page|
# Example:
# scope.per(per_page).page(current_page)
end
# Example: let's say we're hitting an HTTP
# service instead of ActiveRecord. Maybe our
# scope is a big hash we build up, and now
# we need to use a client to take that hash and
# execute the HTTP request.
def resolve(scope)
MyHTTPClient.request(scope)
end
end
Serializers
Serializers define the structure of the response.
We use jsonapi-rb for serialization. If you're familiar with active_model_serializers, the interface will seem very familiar. In fact, jsonapi-rb was created by one of the owners of that project.
See also:
class PostSerializer < ApplicationSerializer
type 'foos'
attribute :title
attribute :description do
@object.active? ? 'Active Post' : 'Inactive Post'
end
extra_attribute :net_worth do
@object.assets.sum(&:value)
end
has_many :comments do
data do
@object.published_comments
end
link :related do
@url_helpers.comments_url(filter: { post_id: @object.id })
end
end
meta do
{ featured: true }
end
end
Sideloading
You're probably already familiar with the concept of "sideloading", documented in the "Inclusion of Related Resources" section of the JSONPI spec.
If using an
Adapter
, such as the default
ActiveRecord
adapter,
you can do this with simple
has_many
-style macros.
If not using an adapter, or to implement a one-off customization,
you can drop down to the lower-level
allow_sideload
DSL. Adapter
macros are simply wrapping this DSL.
allow_sideload
specifies two simple
configuration options:
- scope: specifies how to build a scope that satisifes the association.
- assign: specifies how to associate the resulting records with their parent association.
You can see the default
ActiveRecord
implementation on the right.
See also:
# Adapter example
has_many :comments,
foreign_key: :post_id,
resource: CommentResource,
scope: -> { Comment.all } # base scope, like you'd see in a controller
# Lower-level customization example
allow_sideload :blog, resource: BlogResource do
# Return a scope so further query parameters can be applied
# and the Resource logic can be re-used
scope do |posts|
Blog.where(id: posts.map(&:blog_id))
end
assign do |posts, blogs|
posts.each do |p|
p.blog = blogs.find { |b| b.id == p.blog_id }
end
end
end
Sideposting
We want to be able to *write* a nested relationship graph.
You can do this by mirroring the same
sideloading payload
you see in read requests. To the right, you'll see we're updating a
Post
, changing the name of its associated
Blog
, creating a
Tag
, deleting one
Comment
, and disassociating (null
foreign key) a different
Comment
, all in a single request.
When we send RESTful resources to the server, we typically send a corresponding HTTP verb.
That's the
method
section of the payload.
method
can be either
create
,
update
,
destroy
, or
disassociate
.
When creating, there is no
id
to pass. Instead, pass
temp-id
, a random uuid used to
link the relevant sections of the payload, and which tells clients how to associate
their in-memory objects with the
id
s returned from the server. Clients like
JSORM will take care of this for you automatically.
When we actually perform these operations, everything will run within a transaction. The
transaction will be rolled back if an error is raised or the
Model
s, do not pass validation.
See also:
{
data: {
type: 'posts',
id: 123,
attributes: { title: 'Updated!' },
relationships: {
blog: {
data: { type: 'blogs', id: 123, method: 'update' }
},
tags: {
data: [{ type: 'tags', temp-id: 's0m3uu1d', method: 'create' }]
},
comments: {
data: [
{ type: 'comments', id: 123, method: 'destroy' }
{ type: 'comments', id: 456, method: 'disassociate' }
]
}
}
},
included: [
{ type: 'tags', temp-id: 's0m3uu1d', attributes: { name: 'Important' } },
{ type: 'blogs', id: 123, attributes: { name: 'Updated!' } }
]
}