tiagopog/jsonapi-utils

Customized resource filters are not applied

griley opened this issue · 9 comments

It appears that trying to customize a filter doesn't work. Given a resource that looks like this:

class UserResource < JSONAPI::Resource
  attributes :first_name, :last_name, :full_name

  attribute :full_name

  has_many :posts

  filter :first_name, apply: ->(records, value, _options) {
    # customize how the filter is applied - this never runs
    records.where(first_name: 'Steve')
  }

  def full_name
    "#{@model.first_name} #{@model.last_name}"
  end
end

Assuming a stock controller and routes, the following request:

/users?filter[first_name]=Mark

results in the default filter being applied, not the customized lambda specified.

Hey there, @griley!

As you mentioned, JSON::Utils only applies equality filters – like the one in the example – automatically. Unfortunately JU doesn't provide any fancy macro to customize filters since it doesn't assume any responsibility over your controller action's operation.

For example, in our production JSON API, when we need to apply a very specific filter, like filtering records by a given scope, we apply that filter at the interactor/service object level. The reason for such decision is that we understand that it's a good pattern do make non-trivial filters explicit in the context where the operation of the action happens.

Hope it helps you and if you have any other ideas, just let me know.

Thanks @tiagopog . I agree with the decision to make the filters explicit.

However, the problem I'm running into (and the origin of the issue above) is that the default equality filter is still scoped to any custom filtering I may do in a controller.

For example:

class UserResource < JSONAPI::Resource
  attributes :first_name, :last_name, :full_name
  has_many :posts

  # I'd like to expose a filter on a derived attribute
  filter :full_name

  def full_name
    "#{@model.first_name} #{@model.last_name}"
  end
end
class UsersController < BaseController
  # GET /users
  def index
    jsonapi_render json: filtered_users
  end

  private

  def filtered_users
    first_name, last_name = params[:filter][:full_name].split
    User.where(first_name: first_name, last_name: last_name)
  end
end

When filtered_users is run above, the scope implied by the filter in UserResource is also applied to it. Resulting in:

User.where(first_name: first_name, last_name: last_name).where(full_name: params[:filter][:full_name])

Obviously, this is a problem because the attribute full_name doesn't actually exist.

Any suggestion on a way around this or a different approach?

I guess I could unscope(where: :full_name) in filtered_users but that smells a bit to me.

Ok, now I gotcha! In this case you might simply skip the default equality filter: jsonapi_render json: filtered_users, options: { filter: false }

You might apply the same to pagination (options: { paginate: false }). I'm sorry for the lack of documentation for this feature. I may write something covering this case in the README.md later.

Thanks for the quick response @tiagopog and pointers!

The jsonapi_render json: filtered_users, options: { filter: false } addressed my issue.

I finally realized why Tiago suggested setting a pagination option after I enabled the pagination feature in the initializer:

config.default_paginator = :paged

The record count information applies the same default filter to it. But even after adding the paginate: false option, it continued to occur. I sniffed around the code a little bit and instead tried to add my own custom record count:

jsonapi_render json: filtered_users, options: { filter: false , count: filtered_users.length }

However, this line of code is thwarting that feature because it's overwriting the options I passed in.

@griley, thanks for reporting it. I might take a look on this possible bug later as well as I'll try to remember if there was any particular motivation behind this line of code.

On a side note...

If the options weren't reinitialized, the apply_filters(records, options) found here would cause the record count to be calculated correctly because of the filter: false

This would obviate the need for a count option in this issue.

Thanks, and good luck on Sunday in the football match vs Germany ;)