class Insights::API::Common::Filter

Constants

ALL_COMPARISON_KEYWORDS
INTEGER_COMPARISON_KEYWORDS
STRING_COMPARISON_KEYWORDS

Attributes

api_doc_definition[R]
apply[R]
arel_table[R]
extra_filterable_attributes[R]
model[R]
query[W]

Public Class Methods

association_attribute_properties(api_doc_definitions, raw_filter) click to toggle source
# File lib/insights/api/common/filter.rb, line 88
def self.association_attribute_properties(api_doc_definitions, raw_filter)
  return {} if raw_filter.blank?

  association_attributes = compact_filter(raw_filter).keys.select { |key| key.include?('.') }.compact.uniq
  return {} if association_attributes.blank?

  association_attributes.each_with_object({}) do |key, hash|
    association, attr = key.split('.')
    hash[key] = api_doc_definitions[association.singularize.classify].properties[attr.to_s]
  end
end
compact_filter(filter) click to toggle source

Compact filters to support association filtering

Input:  {"source_type"=>{"name"=>{"eq"=>"rhev"}}}
Output: {"source_type.name"=>{"eq"=>"rhev"}}

Input:  {"source_type"=>{"name"=>{"eq"=>["openstack", "openshift"]}}}
Output: {"source_type.name"=>{"eq"=>["openstack", "openshift"]}}
# File lib/insights/api/common/filter.rb, line 65
def self.compact_filter(filter)
  result = {}
  return result if filter.blank?
  return filter unless filter.kind_of?(Hash) || filter.kind_of?(ActionController::Parameters)

  filter = Hash(filter.permit!) if filter.kind_of?(ActionController::Parameters)

  filter.each do |ak, av|
    if av.kind_of?(Hash)
      av.each do |atk, atv|
        if !ALL_COMPARISON_KEYWORDS.include?(atk)
          result["#{ak}.#{atk}"] = atv
        else
          result[ak] = av
        end
      end
    else
      result[ak] = av
    end
  end
  result
end
new(model, raw_filter, api_doc_definition, extra_filterable_attributes = {}) click to toggle source

Instantiates a new Filter object

Parameters:

model

An AR model that acts as the base collection to be filtered

raw_filter

The filter from the request query string

api_doc_definition

The documented object definition from the OpenAPI doc

extra_filterable_attributes

Attributes that can be used for filtering but are not documented in the OpenAPI doc. Something like `{“undocumented_column” => {“type” => “string”}}`

Returns:

A new Filter object, call apply to get the filtered set of results.

# File lib/insights/api/common/filter.rb, line 26
def initialize(model, raw_filter, api_doc_definition, extra_filterable_attributes = {})
  @raw_filter                  = raw_filter
  @api_doc_definition          = api_doc_definition
  @arel_table                  = model.arel_table
  @extra_filterable_attributes = extra_filterable_attributes
  @model                       = model
end

Private Class Methods

build_filtered_scope(scope, api_version, klass_name, filter) click to toggle source
# File lib/insights/api/common/filter.rb, line 180
def self.build_filtered_scope(scope, api_version, klass_name, filter)
  return scope unless filter

  openapi_doc = ::Insights::API::Common::OpenApi::Docs.instance[api_version]
  openapi_schema_name, = ::Insights::API::Common::GraphQL::Generator.openapi_schema(openapi_doc, klass_name)

  action_parameters = ActionController::Parameters.new(filter)
  definitions = openapi_doc.definitions

  association_attribute_properties = association_attribute_properties(definitions, action_parameters)

  new(scope, action_parameters, definitions[openapi_schema_name], association_attribute_properties).apply
end

Public Instance Methods

query() click to toggle source
# File lib/insights/api/common/filter.rb, line 34
def query
  @query ||= filter_associations.present? ? model.left_outer_joins(filter_associations) : model
end

Private Instance Methods

add_filter(requested_comparator, allowed_comparators, key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 165
def add_filter(requested_comparator, allowed_comparators, key, value)
  return unless attribute = attribute_for_key(key)
  type = determine_string_attribute_type(attribute)

  if requested_comparator.in?(["nil", "not_nil"])
    send("comparator_#{requested_comparator}", key, value)
  elsif requested_comparator.in?(allowed_comparators)
    value = parse_datetime(value) if type == :datetime
    return if value.nil?
    send("comparator_#{requested_comparator}", key, value)
  else
    errors << "unsupported #{type} comparator: #{requested_comparator}"
  end
end
arel_lower(key) click to toggle source
# File lib/insights/api/common/filter.rb, line 223
def arel_lower(key)
  Arel::Nodes::NamedFunction.new("LOWER", [model_arel_attribute(key)])
end
attribute_for_key(key) click to toggle source
# File lib/insights/api/common/filter.rb, line 137
def attribute_for_key(key)
  attribute = api_doc_definition.properties[key.to_s]
  attribute ||= extra_filterable_attributes[key.to_s]
  return attribute if attribute
  errors << "found unpermitted parameter: #{key}"
  nil
end
comparator_contains(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 227
def comparator_contains(key, value)
  return value.each { |v| comparator_contains(key, v) } if value.kind_of?(Array)

  self.query = query.where(model_arel_attribute(key).matches("%#{query.sanitize_sql_like(value)}%", nil, true))
end
comparator_contains_i(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 233
def comparator_contains_i(key, value)
  return value.each { |v| comparator_contains_i(key, v) } if value.kind_of?(Array)

  self.query = query.where(model_arel_table(key).grouping(arel_lower(key).matches("%#{query.sanitize_sql_like(value.downcase)}%", nil, true)))
end
comparator_ends_with(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 247
def comparator_ends_with(key, value)
  self.query = query.where(model_arel_attribute(key).matches("%#{query.sanitize_sql_like(value)}", nil, true))
end
comparator_ends_with_i(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 251
def comparator_ends_with_i(key, value)
  self.query = query.where(model_arel_table(key).grouping(arel_lower(key).matches("%#{query.sanitize_sql_like(value.downcase)}", nil, true)))
end
comparator_eq(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 255
def comparator_eq(key, value)
  self.query = query.where(model_arel_attribute(key).eq_any(Array(value)))
end
comparator_eq_i(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 269
def comparator_eq_i(key, value)
  values = Array(value).map { |v| query.sanitize_sql_like(v.downcase) }

  self.query = query.where(model_arel_table(key).grouping(arel_lower(key).matches_any(values)))
end
comparator_gt(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 275
def comparator_gt(key, value)
  self.query = query.where(model_arel_attribute(key).gt(value))
end
comparator_gte(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 279
def comparator_gte(key, value)
  self.query = query.where(model_arel_attribute(key).gteq(value))
end
comparator_lt(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 283
def comparator_lt(key, value)
  self.query = query.where(model_arel_attribute(key).lt(value))
end
comparator_lte(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 287
def comparator_lte(key, value)
  self.query = query.where(model_arel_attribute(key).lteq(value))
end
comparator_nil(key, _value = nil) click to toggle source
# File lib/insights/api/common/filter.rb, line 291
def comparator_nil(key, _value = nil)
  self.query = query.where(model_arel_attribute(key).eq(nil))
end
comparator_not_eq(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 259
def comparator_not_eq(key, value)
  self.query = query.where.not(model_arel_attribute(key).eq_any(Array(value)))
end
comparator_not_eq_i(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 263
def comparator_not_eq_i(key, value)
  values = Array(value).map { |v| query.sanitize_sql_like(v.downcase) }

  self.query = query.where.not(model_arel_table(key).grouping(arel_lower(key).matches_any(values)))
end
comparator_not_nil(key, _value = nil) click to toggle source
# File lib/insights/api/common/filter.rb, line 295
def comparator_not_nil(key, _value = nil)
  self.query = query.where.not(model_arel_attribute(key).eq(nil))
end
comparator_starts_with(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 239
def comparator_starts_with(key, value)
  self.query = query.where(model_arel_attribute(key).matches("#{query.sanitize_sql_like(value)}%", nil, true))
end
comparator_starts_with_i(key, value) click to toggle source
# File lib/insights/api/common/filter.rb, line 243
def comparator_starts_with_i(key, value)
  self.query = query.where(model_arel_table(key).grouping(arel_lower(key).matches("#{query.sanitize_sql_like(value.downcase)}%", nil, true)))
end
determine_string_attribute_type(attribute) click to toggle source
# File lib/insights/api/common/filter.rb, line 145
def determine_string_attribute_type(attribute)
  return :timestamp if attribute["format"] == "date-time"
  return :integer if attribute["pattern"] == /^\d+$/
  :string
end
errors() click to toggle source
# File lib/insights/api/common/filter.rb, line 151
def errors
  @errors ||= []
end
filter_associations() click to toggle source
# File lib/insights/api/common/filter.rb, line 125
def filter_associations
  return nil if @raw_filter.blank?

  @filter_associations ||= begin
    self.class.compact_filter(@raw_filter).keys.collect do |key|
      next unless key.include?('.')

      key.split('.').first.to_sym
    end.compact.uniq
  end
end
integer(k, val) click to toggle source
# File lib/insights/api/common/filter.rb, line 213
def integer(k, val)
  if val.kind_of?(Hash)
    val.each do |comparator, value|
      add_filter(comparator, INTEGER_COMPARISON_KEYWORDS, k, value)
    end
  else
    add_filter("eq", INTEGER_COMPARISON_KEYWORDS, k, val)
  end
end
key_model_attribute(key) click to toggle source
# File lib/insights/api/common/filter.rb, line 106
def key_model_attribute(key)
  if key.include?('.')
    association, attr = key.split('.')
    [association.classify.constantize, attr]
  else
    [model, key]
  end
end
model_arel_attribute(key) click to toggle source
# File lib/insights/api/common/filter.rb, line 115
def model_arel_attribute(key)
  key_model, attr = key_model_attribute(key)
  key_model.arel_attribute(attr)
end
model_arel_table(key) click to toggle source
# File lib/insights/api/common/filter.rb, line 120
def model_arel_table(key)
  key_model, attr = key_model_attribute(key)
  key_model.arel_table
end
parse_datetime(value) click to toggle source
# File lib/insights/api/common/filter.rb, line 204
def parse_datetime(value)
  return value.collect { |i| parse_datetime(i, ) } if value.kind_of?(Array)

  DateTime.parse(value)
rescue ArgumentError
  errors << "invalid timestamp: #{value}"
  return nil
end
string(k, val) click to toggle source
# File lib/insights/api/common/filter.rb, line 155
def string(k, val)
  if val.kind_of?(Hash)
    val.each do |comparator, value|
      add_filter(comparator, STRING_COMPARISON_KEYWORDS, k, value)
    end
  else
    add_filter("eq", STRING_COMPARISON_KEYWORDS, k, val)
  end
end
timestamp(k, val) click to toggle source
# File lib/insights/api/common/filter.rb, line 194
def timestamp(k, val)
  if val.kind_of?(Hash)
    val.each do |comparator, value|
      add_filter(comparator, INTEGER_COMPARISON_KEYWORDS, k, value)
    end
  else
    add_filter("eq", INTEGER_COMPARISON_KEYWORDS, k, val)
  end
end