jsonapi-suite / jsonapi_compliable

MIT License
20 stars 35 forks source link

Resource sideloading 2 #114

Closed richmolj closed 6 years ago

richmolj commented 6 years ago

Use Resource query interface to sideload (by default)

1.0 has the query interface:

records = EmployeeResource.all(filter: { first_name: ‘foo’ })

Which means we can use this interface for sideloading under-the-hood:

employees # => array of resolved employee objects
PositionResource.all(filter: { employee_id: employees.map(&:id) })

This has a few benefits: the filter logic can be re-used, there are less concepts to understand, and makes jumping between resources with different datastores easier (no custom scope/assign required!).

All the existing (meaning 1.0 existing, not < 1.0) customizations can still be applied if you need to drop to a lower-level. If a scope block is given we will avoid this pattern and scope however you'd like. In fact, this is still required for many-to-many relationships, and for complex joins where it doesn't make sense to re-use filters.

This does come at the cost of requiring a filterable foreign key. For now, I'd like to avoid adding this implicitly, but with a little sugar to make it easier:

class EmployeeResource < ApplicationResource
  has_many :positions
end

class PositionResource < ApplicationResource
  # you can use :except too, fwiw
  attribute :employee_id, only: [:filterable]
end

As part of making this work, the Query class was fundamentally refactored. We now rely on methods and avoid to_hash as much as possible. to_hash is only used to generate the required query for the sideload (in order to support deep queries). In addition, we no longer base things on the jsonapi "type" and now walk the graph (so in the future you can load and filter the same entity differently). You can pass a jsonapi type or a relationship name, though this will go away eventually in favor of ?filter[positions.title].

Finally, a bit of a change to the query interface that impacts rendering. It's now:

records = EmployeeResource.all(params)
records.class # JsonapiCompliable::ResourceProxy
records.to_a # fires query, returns records
records.each { ... } # works

def index
  employees = EmployeeResource.all(params)
  render jsonapi: employees
end

def show
  employees = EmployeeResource.find(params)
  render jsonapi: employees
end

Other notes:

richmolj commented 6 years ago

@wadetandy on top of https://github.com/jsonapi-suite/jsonapi_compliable/pull/113

hooptie45 commented 6 years ago

I like the direction that you're trying to go here, but I've got some reservations about the added complexity, and performance impact; have you considered adding some benchmark code before this goes into master?

richmolj commented 6 years ago

Many, many things are going to happen before merging this code to master. The point of this branch is to share my work as it happens.