Module: Refine::Conditions::UsesAttributes

Included in:
Condition, FilterCondition, OptionCondition
Defined in:
app/models/refine/conditions/uses_attributes.rb

Instance Method Summary collapse

Instance Method Details

#apply_relationship_attribute(input:, query:) ⇒ Object



9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'app/models/refine/conditions/uses_attributes.rb', line 9

def apply_relationship_attribute(input:, query:)
  # Split on first .
  decompose_attribute = @attribute.split(".", 2)
  # Attribute now is the back half of the initial attribute
  @attribute = decompose_attribute[1]

  if !@attribute.include?(".")
    # No more .s, deepest level
    @on_deepest_relationship = true
  end
  # Relation to be handled
  relation = decompose_attribute[0]

  # Get the Reflection object which defines the relationship between query and relation
  # First iteration pull relationship using base query which responds to model.
  instance = get_reflection_object(query, relation)

  unless instance
    raise "Relationship does not exist for #{relation}."
  end

  filter.set_pending_relationship(relation, instance)
  
  # If the current condition is a refinement (filter refinement) set collapsible to true 
  if is_refinement
    filter.allow_pending_relationship_to_collapse
  end

  if can_use_where_in_relationship_subquery?(instance)
    create_pending_wherein_subquery(input: input, relation: relation, instance: instance, query: query)
  else
    create_pending_has_many_through_subquery(input: input, relation: relation, instance: instance, query: query)
  end

  filter.release_pending_relationship
  # We want the method to return nil for relationship attributes
  # The purpose of this method is to populate pending relationship subqueries
  nil
end

#can_use_where_in_relationship_subquery?(instance) ⇒ Boolean

Returns:

  • (Boolean)


175
176
177
178
# File 'app/models/refine/conditions/uses_attributes.rb', line 175

def can_use_where_in_relationship_subquery?(instance)
  # Where in only works for belongs to, has one, or has many
  (instance.is_a? ActiveRecord::Reflection::BelongsToReflection) || (instance.is_a? ActiveRecord::Reflection::HasManyReflection) || (instance.is_a? ActiveRecord::Reflection::HasOneReflection)
end

#create_pending_has_many_through_subquery(input:, relation:, instance:, query:) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'app/models/refine/conditions/uses_attributes.rb', line 120

def create_pending_has_many_through_subquery(input:, relation:, instance:, query:)
  # In a has_many relationship the negative has to be flipped to positive. 
  inverse_clause = should_inverse_clause?(instance, input[:clause])
  # Ex: A country has many posts through hmtt_users.
  # Use AR to properly join the relation to the base query provided
  # Convert to AREL to use with nodes 
  subquery_path = query.model.select(key_1(instance)).joins(relation.to_sym).arel
  relation_table_being_queried = instance.klass.arel_table

  relation_class = instance.klass

  if self.through_id_relationship
    if instance.is_a? ActiveRecord::Reflection::ThroughReflection
      through_reflection = instance.through_reflection
      if(through_reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection))
        through_reflection = instance.source_reflection.through_reflection
      end
      parent_foreign_key = through_reflection.foreign_key
      child_foreign_key = instance.source_reflection.foreign_key
      relation_table_being_queried = through_reflection.klass.arel_table
      relation_class = through_reflection.klass
      if(through_reflection.chain.count > 1)
        # We need to shortcut the outer inner joins but allow the others to go as normal
        parent_foreign_key = through_reflection.chain.last.foreign_key
        parent_through_reflection = through_reflection.chain.last
        join_sym = through_reflection.source_reflection_name
        subquery_path = parent_through_reflection.klass.select(parent_foreign_key).joins(join_sym)
        if(through_reflection.scope && through_reflection.scope.is_a?(Proc))
          subquery_path = subquery_path.instance_exec(&through_reflection.scope)
        end
        subquery_path = subquery_path.arel
      else
        # Does not need any inner joins
        subquery_path = through_reflection.klass.select(parent_foreign_key).arel
      end
     
      node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause, child_foreign_key)
    else
      raise Refine::Conditions::Errors::RelationshipError.new("with_through_id must be used with a has_many :through relationship")
    end
  else
    node_to_apply = apply(input, relation_table_being_queried, relation_class, inverse_clause)
  end

  if self.forced_index
    mysql_from_segment = "`#{through_reflection.table_name}` FORCE INDEX(`#{self.forced_index}`)"
    # Ensuring input is sanitized
    subquery_path = subquery_path.from(Arel.sql(mysql_from_segment))
  end
  
  complete_subquery = subquery_path.where(node_to_apply)
  subquery = filter.get_pending_relationship_subquery || complete_subquery
  filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: nil, inverse_clause: inverse_clause)
end

#create_pending_wherein_subquery(input:, relation:, instance:, query:) ⇒ Object



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
# File 'app/models/refine/conditions/uses_attributes.rb', line 76

def create_pending_wherein_subquery(input:, relation:, instance:, query:)
  # This method builds out the linking keys between the provided query model and the relation
  # and saves it to pending relationship subqueries
  # Class of the relation as held in the AR::Relation object
  relation_class = instance.klass

  # Pull what's already in the tracker at this depth if already traversed
  subquery = filter.get_pending_relationship_subquery || relation_class.select([key_2(instance)]).arel

  # Primary/secondary keys keep track of how to link tables
  # If depth has been added (i.e. filter.pending_relationship_subquery_depth = [:btt_user, :btt_notes])
  # This will add the [:children] key to the pending_relationship_subqueries tracker under the parent key [:btt_user]
  filter.add_pending_relationship_subquery(subquery: subquery, primary_key: key_1(instance), secondary_key: key_2(instance))

  # Apply the condition. If a nested relationship, this apply is adding the children key (with values) to the pending_relationship_subqueries tracker
  # due to the recursive nature of the apply method. This is critical because it get is then "rolled up" in release_pending_relationship
  node = apply(input, relation_class.arel_table, relation_class, false)

  # If node is an AREL::SELECT manager we are allowing the apply condition to return a fully formed subquery - we replace
  # the linking keys in the tracker with the fully formed select query
  # Has not been tested more than one level deep
  if node.is_a? Arel::SelectManager
    filter.add_pending_relationship_subquery(subquery: node, primary_key: key_1(instance), secondary_key: key_2(instance))
  elsif node
    # This modifies subquery *in* the pending_relationship_subqueries tracker.
    subquery.where(node)
  end
end

#get_reflection_object(query, relation) ⇒ Object



66
67
68
69
70
71
72
73
74
# File 'app/models/refine/conditions/uses_attributes.rb', line 66

def get_reflection_object(query, relation)
  if query.respond_to? :model
    query.model.reflect_on_association(relation.to_sym)
  else
    # When query is sent in as subquery (recursive) the query object is the model class pulled from the
    # previous instance value
    query.reflect_on_association(relation.to_sym)
  end
end

#group(nodes) ⇒ Object



105
106
107
# File 'app/models/refine/conditions/uses_attributes.rb', line 105

def group(nodes)
  Arel::Nodes::Grouping.new(nodes)
end

#is_relationship_attribute? ⇒ Boolean

Returns:

  • (Boolean)


180
181
182
183
184
185
186
# File 'app/models/refine/conditions/uses_attributes.rb', line 180

def is_relationship_attribute?
  # TODO: Allow user to decide attribute is not a relationship
  # If we are on the deepest relationship, it's no longer a relationship attribute
  return false if @on_deepest_relationship
  # If the attribute includes a ., it's a relationship attribute
  @attribute.include?(".")
end

#key_1(instance) ⇒ Object



49
50
51
52
53
54
55
56
# File 'app/models/refine/conditions/uses_attributes.rb', line 49

def key_1(instance)
  # Foreign key on belongs to, primary key on HasMany
  if instance.is_a? ActiveRecord::Reflection::BelongsToReflection
    instance.foreign_key.to_sym
  else
    instance.active_record_primary_key.to_sym
  end
end

#key_2(instance) ⇒ Object



58
59
60
61
62
63
64
# File 'app/models/refine/conditions/uses_attributes.rb', line 58

def key_2(instance)
  if instance.is_a? ActiveRecord::Reflection::BelongsToReflection
    instance.active_record_primary_key.to_sym
  else
    instance.foreign_key.to_sym
  end
end

#raw_attribute(attribute) ⇒ Object



188
189
190
191
192
# File 'app/models/refine/conditions/uses_attributes.rb', line 188

def raw_attribute(attribute)
  @attribute = Arel.sql(attribute)

  self
end

#should_inverse_clause?(instance, clause) ⇒ Boolean

Determine if the clause should be flipped. For example, "not_eq" => "eq". Must also change "in" to "not in" upstream

Parameters:

  • instance (Object)
  • clause (String) —

    The join clause (example: eq or not_eq)

Returns:

  • (Boolean)


113
114
115
116
117
118
# File 'app/models/refine/conditions/uses_attributes.rb', line 113

def should_inverse_clause?(instance, clause)
  is_through_reflection = instance.is_a?(ActiveRecord::Reflection::ThroughReflection)
  is_inverse_clause_flippable = Clauses::FLIPPABLE.include?(clause)

  is_through_reflection && is_inverse_clause_flippable
end

#with_attribute(value) ⇒ Object



4
5
6
7
# File 'app/models/refine/conditions/uses_attributes.rb', line 4

def with_attribute(value)
  @attribute = value
  self
end