Class: JsonApiServer::Filter
- Inherits:
-
Object
- Object
- JsonApiServer::Filter
- 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
-
#model ⇒ Object
readonly
ActiveRecord::Base model passed in constructor.
-
#params ⇒ Object
readonly
Query parameters from #request.
-
#permitted ⇒ Object
readonly
Filter configs passed in constructor.
-
#request ⇒ Object
readonly
ActionDispatch::Request passed in constructor.
Instance Method Summary collapse
-
#filter_params ⇒ Object
Filter params from query parameters.
-
#initialize(request, model, permitted = []) ⇒ Filter
constructor
Arguments: -
request
- ActionDispatch::Request -model
- ActiveRecord::Base model. -
#meta_info ⇒ Object
Hash with filter meta information.
-
#relation ⇒ Object
(also: #query)
Returns an ActiveRecord Relation object (query fragment) which can be merged with another.
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
#model ⇒ Object (readonly)
ActiveRecord::Base model passed in constructor.
176 177 178 |
# File 'lib/json_api_server/filter.rb', line 176 def model @model end |
#params ⇒ Object (readonly)
Query parameters from #request.
173 174 175 |
# File 'lib/json_api_server/filter.rb', line 173 def params @params end |
#permitted ⇒ Object (readonly)
Filter configs passed in constructor.
179 180 181 |
# File 'lib/json_api_server/filter.rb', line 179 def permitted @permitted end |
#request ⇒ Object (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_params ⇒ Object
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_info ⇒ Object
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 ||= begin { filter: filter_params.each_with_object([]) do |(attr, val), result| result << "#{attr}: #{val}" if attr.present? && val.present? end } end end |
#relation ⇒ Object 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 |