class JsonApiServer::Filter

Implements filter parameters per JSON API Spec: jsonapi.org/recommendations/#filtering. The spec says: “The filter query parameter is reserved for filtering data. Servers and clients SHOULD use this key for filtering operations.”

ie., GET /topics?filter[id]=1,2&filter[book]=*potter

This class (1) whitelists filter params and (2) generates a sub-query based on filters. If a user requests an unsupported filter, a JsonApiServer::BadRequest exception is raised which renders a 400 error.

Currently supports only ActiveRecord::Relation. Filters are combined with AND.

Usage:

A filter request will look like:

/topics?filter[id]=1,2&filter[book]=*potter

Configurations:

Configurations look like:

[
 { id: { type: 'Integer' } },
 { tags: { builder: :pg_jsonb_ilike_array } },
 { published: { type: 'Date' } },
 { published1: { col_name: :published, type: 'Date' } },
 :location,
 { book: { wildcard: :both } },
 { search: { builder: :model_query, method: :search } }
]
whitelist

Filter attributes are whitelisted. Specify filters you want to support in filter configs.

:type

:type (data type) defaults to String. Filter values are cast to this type. Supported types are:

:col_name

If a filter name is different from its model column/attribute, specify the column/attribute with :col_name.

:wildcard

A filter can enable wildcarding with the :wildcard option. :both wildcards both sides, :left wildcards the left, :right wildcards the right. A user triggers wildcarding by preceding a filter value with a * character (i.e., *weather).

/comments?filter[comment]=*weather => "comments"."comment" LIKE '%weather%'

Additional wildcard/like filters are available for Postgres.

ILIKE for case insensitive searches:

For searching a JSONB array - case sensitive:

For searching a JSONB array - case insensitive:

builder: :model_query

A filter can be configured to call a model's singleton method.

Example:

[
 { search: { builder: :model_query, method: :search } }
]

Request:

/comments?filter[search]=tweet

The singleton method search will be called on the model specified in the filter constructor.

builder:

Specify a specific filter builder to handle the query. The list of default builders is in JsonApiServer::Configuration.

[
  { tags: { builder: :pg_jsonb_ilike_array } }
]

As mentioned above, there are additional filter builders for Postgres. Custom filter builders can be added. In this example, it's using the :pg_jsonb_ilike_array builder which performs a case insensitve search on a JSONB array column.

Features

IN statement

Comma separated filter values translate into an IN statement.

/topics?filter[id]=1,2 => "topics"."id" IN (1,2)'
Operators

The following operators are supported:

=, <, >, >=, <=, !=

Example:

/comments?filter[id]=>=20
# note: special characters should be encoded -> /comments?filter[id]=%3E%3D20
Searching a Range

Searching a range can be achieved with two filters for the same model attribute and operators:

Configuration:

[
 { published: { type: 'Date' } },
 { published1: { col_name: :published, type: 'Date' } }
]

Request:

/topics?filter[published]=>1998-01-01&filter[published1]=<1999-12-31

Produces a query like:

("topics"."published" > '1998-01-01') AND ("topics"."published" < '1999-12-31')

Custom Filters

Custom filters can be added. Filters should inherit from JsonApiServer::FilterBuilder.

Example:

# In config/initializers/json_api_server.rb

# Create custom fitler.
module JsonApiServer
 class MyCustomFilter < FilterBuilder
   def to_query(model)
     model.where("#{full_column_name(model)} LIKE :val", val: "%#{value}%")
   end
 end
end

# Update :filter_builders attribute to include your builder.
JsonApiServer.configure do |c|
 c.base_url = 'http://localhost:3001'
 c.filter_builders = c.filter_builders.merge(my_custom_builder: JsonApiServer::MyCustomFilter)
 c.logger = Rails.logger
end

# and then use it in your controllers...
#  c.filter_options = [
#  { names: { builder: :my_custom_builder } }
# ]

Note:

Attributes

model[R]

ActiveRecord::Base model passed in constructor.

params[R]

Query parameters from request.

permitted[R]

Filter configs passed in constructor.

request[R]

ActionDispatch::Request passed in constructor.

Public Class Methods

new(request, model, permitted = []) click to toggle source

Arguments:

  • request - ActionDispatch::Request

  • model - ActiveRecord::Base model. Used to generate sub-query.

  • permitted (Array) - Defaults to empty array. Filter configurations.

# File lib/json_api_server/filter.rb, line 185
def initialize(request, model, permitted = [])
  @request = request
  @model = model
  @permitted = permitted.is_a?(Array) ? permitted : []
  @params = request.query_parameters
end

Public Instance Methods

filter_params() click to toggle source

Filter params from query parameters.

# File lib/json_api_server/filter.rb, line 193
def filter_params
  @filter ||= params[:filter] || {}
end
meta_info() click to toggle source

Hash with filter meta information. It echos untrusted user input (no sanitizing).

i.e.,

{
  filter: [
    'id: 1,2',
    'comment: *weather'
  ]
}
# File lib/json_api_server/filter.rb, line 222
def meta_info
  @meta_info ||= begin
    { filter:
    filter_params.each_with_object([]) do |(attr, val), result|
      result << "#{attr}: #{val}" if attr.present? && val.present?
    end }
  end
end
query()
Alias for: relation
relation() click to toggle source

Returns an ActiveRecord Relation object (query fragment) which can be merged with another.

# File lib/json_api_server/filter.rb, line 199
def relation
  @conditions ||= begin
    filter_params.each_with_object(model.all) do |(attr, val), result|
      if attr.present? && val.present?
        query = query_for(attr, val)
        result.merge!(query) unless query.nil? # query.present? triggers a db call.
      end
    end
  end
end
Also aliased as: query

Protected Instance Methods

config_for(attr) click to toggle source

Returns config information on permitted attributes. Raises JsonApiServer::BadRequest with descriptive message if attribute is not whitelisted.

# File lib/json_api_server/filter.rb, line 244
def config_for(attr)
  config = permitted.find do |a|
    attr == (a.respond_to?(:keys) ? a.keys.first : a).to_s
  end
  if config.nil?
    msg = I18n.t('json_api_server.render_400.filter', param: attr)
    raise JsonApiServer::BadRequest, msg
  end
  FilterConfig.new(config)
end
query_for(attr, val) click to toggle source

Use classes. Allow classes to be pushed in via initializers.

# File lib/json_api_server/filter.rb, line 234
def query_for(attr, val)
  config = config_for(attr)
  return nil if config.nil?
  parser = FilterParser.new(attr, val, model, config)
  parser.to_query
end