Class: JsonApiServer::Filter

Inherits:
Object
  • Object
show all
Defined in:
lib/json_api_server/filter.rb

Overview

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:

  • String

  • Integer

  • Date (Note: invalid Date casts to nil)

  • DateTime (Note: invalid DateTime casts to nil)

  • Float (untested)

  • BigDecimal (untested)

: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:

  • pg_ilike: JsonApiServer::PgIlike

For searching a JSONB array - case sensitive:

  • pg_jsonb_array: JsonApiServer::PgJsonbArray

For searching a JSONB array - case insensitive:

  • pg_jsonb_ilike_array: JsonApiServer::PgJsonbIlikeArray

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:

  • JsonApiServer::Builder class provides an easier way to use this class.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(request, model, permitted = []) ⇒ Filter

Arguments:

  • request - ActionDispatch::Request

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

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



185
186
187
188
189
190
# 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

Instance Attribute Details

#modelObject (readonly)

ActiveRecord::Base model passed in constructor.



176
177
178
# File 'lib/json_api_server/filter.rb', line 176

def model
  @model
end

#paramsObject (readonly)

Query parameters from #request.



173
174
175
# File 'lib/json_api_server/filter.rb', line 173

def params
  @params
end

#permittedObject (readonly)

Filter configs passed in constructor.



179
180
181
# File 'lib/json_api_server/filter.rb', line 179

def permitted
  @permitted
end

#requestObject (readonly)

ActionDispatch::Request passed in constructor.



170
171
172
# File 'lib/json_api_server/filter.rb', line 170

def request
  @request
end

Instance Method Details

#filter_paramsObject

Filter params from query parameters.



193
194
195
# File 'lib/json_api_server/filter.rb', line 193

def filter_params
  @filter ||= params[:filter] || {}
end

#meta_infoObject

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

i.e.,

{
  filter: [
    'id: 1,2',
    'comment: *weather'
  ]
}


222
223
224
225
226
227
228
229
# 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

#relationObject Also known as: query

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



199
200
201
202
203
204
205
206
207
208
# 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