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 ids 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 Models, 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!' } }
  ]
}