Class: GraphqlDeclarative::Filter

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

Overview

Applies a FilterInput's arguments to an ActiveRecord scope.

THE IMPORTANT PART — see spec/pagination_through_associations_spec.rb. Filters declared with through: MUST NOT be applied as a join on the scope being paginated. Resolve them to an id subquery instead:

ids = model.joins(through).where(assoc_table => {column => value}).select(:id)
scope.where(id: ids)

The base relation then stays un-joined, so LIMIT, ORDER BY and cursors all remain correct. Two through: filters on the same association must intersect within ONE subquery, not chain two (that changes the meaning from "one child matching both" to "children matching each").

Chosen semantic, stated once so it is not rediscovered by accident: ONE CHILD ROW MUST SATISFY ALL PREDICATES DECLARED FOR THAT ASSOCIATION.

Constant Summary collapse

LIKE_PATTERNS =

Ops that compare with a LIKE pattern. The pattern is built here, from a sanitized value, and passed as a bound/quoted node — never spliced into SQL text.

{
  contains: ->(v) { "%#{v}%" },
  starts_with: ->(v) { "#{v}%" },
  ends_with: ->(v) { "%#{v}" }
}.freeze
LIKE_ESCAPE =

LIKE's own metacharacters are escaped so a user-supplied "100%" means the literal string, not "100 followed by anything". ESCAPE '\' is emitted explicitly because only MySQL assumes backslash by default.

"\\"

Class Method Summary collapse

Class Method Details

.apply(scope, filter_class, args) ⇒ Object

scope - ActiveRecord::Relation (or a model class) filter_class - a GraphqlDeclarative::FilterInput subclass args - the filter: input, as a Hash with symbol keys, a GraphQL::Schema::InputObject, or nil



40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/graphql_declarative/filter.rb', line 40

def apply(scope, filter_class, args)
  args = normalize_args(args)
  relation = scope.respond_to?(:all) ? scope.all : scope
  return relation if args.empty?

  model = relation.klass
  index = argument_index(filter_class)

  direct = []
  # Keyed by association name so that every predicate on one association
  # ends up in the SAME subquery. This grouping IS the semantic.
  through = Hash.new { |h, k| h[k] = [] }

  args.each do |key, value|
    definition, op = index[key.to_sym]
    unless definition
      raise Error, "unknown filter argument #{key.inspect} for #{filter_class}; " \
                   "known arguments: #{index.keys.sort.inspect}"
    end
    # A key that was not supplied is absent from the hash; an explicit nil
    # is treated as "not filtering on this", not as `IS NULL`.
    next if value.nil?

    (definition.through ? through[definition.through] : direct) << [definition, op, value]
  end

  relation = apply_direct(relation, model, direct)
  apply_through(relation, model, through)
end