Class: RESTFramework::Filters::QueryFilter
- Inherits:
-
BaseFilter
- Object
- BaseFilter
- RESTFramework::Filters::QueryFilter
- Defined in:
- lib/rest_framework/filters/query_filter.rb
Overview
A simple filtering backend that supports filtering a recordset based on query parameters.
Defined Under Namespace
Classes: Not
Constant Summary collapse
- PREDICATES =
{ true: true, false: false, null: nil, lt: ->(f, v) { { f => ...v } }, # `gt` must negate `lte` because Rails doesn't support `>` with endless ranges. # Ref: https://github.com/rails/rails/pull/47345 gt: ->(f, v) { Not.new({ f => ..v }) }, lte: ->(f, v) { { f => ..v } }, gte: ->(f, v) { { f => v.. } }, not: ->(f, v) { Not.new({ f => v }) }, cont: ->(f, v) { [ "#{ActiveRecord::Base.connection.quote_column_name(f)} LIKE ?", "%#{ ActiveRecord::Base.sanitize_sql_like(v) }%" ] }, in: ->(f, v) { if v.is_a?(Array) { f => v.map { |el| el == "null" ? nil : el } } elsif v.is_a?(String) { f => v.split(",").map { |el| el == "null" ? nil : el } } end }, }.freeze
- PREDICATES_REGEX =
/^(.*)_(#{PREDICATES.keys.join("|")})$/- ARRAY_PREDICATES =
Predicates whose value may be an array (e.g.
?id_in[]=1&id_in[]=2). Every other predicate operates on a single scalar and skips array input, which would otherwise raise:continsanitize_sql_like, the range predicates while casting the endpoint. %i[in not].freeze
Instance Method Summary collapse
- #_get_fields ⇒ Object
-
#_get_query_config ⇒ Object
Filter params for keys allowed by the current action's filter_fields/fields config and return a query config in the form of:
[base_query, pred_queries, includes]. -
#filter_data(data) ⇒ Object
Filter data according to the request query parameters.
-
#parse_predicate(field) ⇒ Object
Helper to find a variation of a field using a predicate.
Methods inherited from BaseFilter
#_polymorphic_columns, _safe_query_value?, #initialize
Constructor Details
This class inherits a constructor from RESTFramework::Filters::BaseFilter
Instance Method Details
#_get_fields ⇒ Object
39 40 41 42 |
# File 'lib/rest_framework/filters/query_filter.rb', line 39 def _get_fields # Always return a list of strings; `@controller.readable_columns_or_associations` already does. @controller.class.filter_fields&.map(&:to_s) || @controller.readable_columns_or_associations end |
#_get_query_config ⇒ Object
Filter params for keys allowed by the current action's filter_fields/fields config and return a
query config in the form of: [base_query, pred_queries, includes].
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/rest_framework/filters/query_filter.rb', line 58 def _get_query_config fields = self._get_fields poly_columns = self._polymorphic_columns(@controller.class.filter_fields) includes = [] # Predicate queries must be added to a separate list because multiple predicates can be used. # E.g., `age_lt=10&age_gte=5` would transform to `[{age: ...10}, {age: 5..}]` to avoid conflict # on the `age` key. pred_queries = [] base_query = @controller.request.query_parameters.map { |field, v| # Skip params whose values aren't bind-safe (e.g. a user submitted # `?field[evil]=x`, which Rack parses into a Hash). AR can't quote # those, and the predicate lambdas below would also blow up on them. next nil unless self.class._safe_query_value?(v) # First, if field is a simple filterable field, return early. if field.in?(fields) next [ field, v ] end # A polymorphic association's `<name>.id`/`<name>.type` maps to a backing column on the base # table, so it filters directly with no JOIN (unlike other sub-fields). if column = poly_columns[field] next [ column, v ] end # First, try to parse a simple predicate and check if it is filterable. pred_field, predicate = self.parse_predicate(field) if predicate && pred_field.in?(fields) field = pred_field elsif predicate && (column = poly_columns[pred_field]) field = column else # Last, try to parse a sub-field or sub-field w/predicate. root_field, sub_field = field.split(".", 2) _, pred_sub_field = pred_field.split(".", 2) if predicate # Check if sub-field or sub-field w/predicate is filterable. if sub_field next nil unless root_field.in?(fields) association_fields = @controller.class.field_configuration[root_field][:fields] || [] if sub_field.in?(association_fields) includes << root_field.to_sym next [ field, v ] elsif pred_sub_field && pred_sub_field.in?(association_fields) includes << root_field.to_sym field = pred_field else next nil end else next nil end end # If we get here, we must have a predicate, either from a field or a sub-field. Transform the # value into a query that can be used in the ActiveRecord `where` API. cfg = PREDICATES[predicate.to_sym] if cfg.is_a?(Proc) # Skip a scalar predicate given an array value (Rack parses `?field_cont[]=a&field_cont[]=b` # into an array); only `in`/`not` accept arrays, the rest would raise. next nil if v.is_a?(Array) && !predicate.to_sym.in?(ARRAY_PREDICATES) pred_queries << cfg.call(field, v) else pred_queries << { field => cfg } end next nil }.compact.to_h.symbolize_keys return base_query, pred_queries, includes end |
#filter_data(data) ⇒ Object
Filter data according to the request query parameters.
135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 |
# File 'lib/rest_framework/filters/query_filter.rb', line 135 def filter_data(data) base_query, pred_queries, includes = self._get_query_config if base_query.any? || pred_queries.any? if includes.any? data = data.includes(*includes) end data = data.where(**base_query) if base_query.any? pred_queries.each do |q| if q.is_a?(Not) data = data.where.not(q.q) else data = data.where(q) end end end data end |
#parse_predicate(field) ⇒ Object
Helper to find a variation of a field using a predicate. For example, there could be a field
called age, and if age_lt it passed, we should return ["age", "lt"]. Otherwise, if
something like age is passed, then we should return ["age", nil].
47 48 49 50 51 52 53 54 |
# File 'lib/rest_framework/filters/query_filter.rb', line 47 def parse_predicate(field) if match = PREDICATES_REGEX.match(field) field = match[1] predicate = match[2] end return field, predicate end |