tiagopog/jsonapi-utils

How can we use custom filters?

daniffig opened this issue · 6 comments

I'm having some trouble while filtering. I need my resource to be filtered by its name using a LIKE query (autocomplete field). If I implement JSONAPI::Resources approach, using a lambda, it doesn't work, the lambda it's not called. JSONAPI::Utils provides a custom_filters method, which allows me to apply the filter in the controller, but I don't think that is the best solution. Am I missing something? I can't find any further documentation. Could it be possible to implement all custom filtering code in the resource instead of the controller, as JR proposes?

Hi there, @daniffig. Sorry for the long late on answering you. I'm having some pretty busy days here, but I'll try to be quicker.

Yes, custom_filter(s) are present in the gem but it's an unofficial feature because a proper set of specs and documentation are still lacking. It's working well though.

As you mentioned, custom_filter allows you to enter a filter via controller but it will skip applying an equality comparison (filter[foo]=bar => WHERE foo = 'bar') on the database under the hood.

Let me show a quick usage example:

# app/resources/user_resource.rb
class UserResource < JSONAPI::Resource
  attributes :first_name, :last_name
  custom_filter :name
end

# app/controllers/users_controller.rb
class UsersController < BaseController
  # GET /users
  def index
    searcher = UserSearcher.new(collection: User.all, name: params.dig(:filter, :name)
    jsonapi_render json: searcher.call
  end
end

I may be adding some specs + documentation around this feature soon. I hope this helped you.

I'm using it as you explained and it works great. However, IMO it would be better if we hooked the custom filters to the filter chain, so they can be resolved by the resource, just as JR proposes.

So we could code something like this.

# app/resources/user_resource.rb
class UserResource < JSON::Resource
  attributes :first_name, :last_name
  custom_filter :name

  def self.name(records, value, _options)
    records.where("users.first_name LIKE ?", "%#{value}%")
  end
end

For now JU doesn't push a design pattern for achieving such a feature, thus letting devs to put their logic where they think it's better (i.e. service object, interactor or even controllers).

The initial idea of using the resource layer from JR was to take advantage of its internal DSL only for describing the resource's properties like its attributes, relationships, filters and for some serialization-related stuff. Thus the operations (like that sample custom filter) should rather go to a layer where it makes more sense for an usual Rails application.

Anyway, I may consider what you've suggested and bring this feature as something optional. And by this I mean: if the UserResource class responds to :name, then it tries to apply the custom filter defined there, otherwise it lets the dev define the custom filter method anywhere else. What do you think?

Thanks for the feedback.

I think that's a great proposal.

I've developed a custom server plugin for Symfony 1.4 in the past using JSON API v0.9 definitions, and when the API usage grows the need of complex filters does it too. Clauses like >, <, LIKE, BETWEEN are a must-have, you just can't avoid them if your resource represents large collections. Since JSON API allows to use the reserved word 'filter' as you please, we implemented a filter object with 'field', 'value', 'method' as its params. A preprocessor then converted it to an ActiveRecord query. I think JR approach is cleaner than the one we used, however filters like :name_contains, :created_since, or :updated_between are common word and should have a simple solution.

Thank you for such a great gem!

I needed to filter by multiple values. i added a the following before filter in my controller. The values are pipe separated or comma separated in that order, so if my filter values might have commas in them, i user pipe instead.

before_action :process_filters, only: [:index]

def process_filters
    if params[:filter].present?
      params[:filter].each do |k, v|
        values = v.try(:split, '|')
        values = v.try(:split, ',') if values.size <= 1
        params[:filter][k] = values
      end
    end
  end

The initial idea of using the resource layer from JR was to take advantage of its internal DSL only for describing the resource's properties like its attributes, relationships, filters and for some serialization-related stuff.

But JR supports custom filters on the resource level, using apply/verify. Current behavior of this gem is doubtful, as it supports "some" filters from JR, but not all of them. And it forces to implement filters in other place, spreading such logic across the project. I wish I'd noticed it before :/