activerecord-filter 6.0.0.3 → 6.1.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4d5c6eebcaf0ce67328ce60b589d3db745379c3e1f9e167a72b6d529c12dd242
4
- data.tar.gz: 12e0734ef2c953958cdbef7725e71273d7f6173abeb9e2155c81eb5096e93f87
3
+ metadata.gz: 0a22c93c0080f2e92aaf6b427e89eb6ac73210bc40304941e7e17373b70c987c
4
+ data.tar.gz: 7180d339c3fed586489cd65a4de59e34d5edaf7860584c12683f144134844470
5
5
  SHA512:
6
- metadata.gz: 4faa92743587408661292c04b9100929519354d194a583a0ba9fadb51e208d895ee0d79c2d188563479ce8dc51892d6678f039644ae41cccdafc53eae28c226d
7
- data.tar.gz: 15acb3d28304602f7c24b2ac966f37cd809a31bd36ba4d0fed0f2a8c8d3cdfebe16eb2ea536309d05a458146b31cde62b8fd9e6c6ee1ff7b82e924c44351ff41
6
+ metadata.gz: 4950a8448ea79f7a1ea671889e1353b28657722b3eab8ba202d6c80b02f0d16a2ee9ca4c7893fd24b9b3a436e7a95c7c7a87568345ae6b565a22e8791cec5b51
7
+ data.tar.gz: a061d579c68764f56729d2afba8a979f67804d285b6e1ac895e6ce20722d382e4a636b583b9598607f57340535baae2b369b4d4fcb67e28e8b0599de46c5adbd
data/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  Installtion
6
6
  -----------
7
7
 
8
- - Add `gem 'activerecord-filter', require: 'active_record/filter'
8
+ - Add `gem 'activerecord-filter', require: 'active_record/filter'`
9
9
  - Run `bundle install`
10
10
 
11
11
  Examples
@@ -14,48 +14,89 @@ Examples
14
14
  Normal columns:
15
15
 
16
16
  ```ruby
17
- Property.filter(:id => 5).to_sql
18
- # => "...WHERE properties.id = 5"
17
+ Property.filter(id: 5).to_sql
18
+ Property.filter(id: {eq: 5}).to_sql
19
+ Property.filter(id: {equal_to: 5}).to_sql
20
+ # => "... WHERE properties.id = 5 ..."
19
21
 
20
- Property.filter(:id => [5, 10, 15]).to_sql
21
- # => "...WHERE properties.id IN (5, 10, 15)"
22
+ Property.filter(id: {not: 5}).to_sql
23
+ Property.filter(id: {neq: 5}).to_sql
24
+ Property.filter(id: {not_equal: 5}).to_sql
25
+ # => "... WHERE properties.id != 5 ..."
22
26
 
23
- Property.filter(:id => {:gt => 5}).to_sql
24
- # => "...WHERE properties.id > 5"
27
+ Property.filter(id: [5, 10, 15]).to_sql
28
+ # => "... WHERE properties.id IN (5, 10, 15) ..."
25
29
 
26
- Property.filter(:id => {:gte => 5}).to_sql
27
- # => "...WHERE properties.id >= 5"
30
+ Property.filter(id: {in: [5, 10, 15]}).to_sql
31
+ # => "... WHERE properties.id IN (5, 10, 15) ..."
28
32
 
29
- Property.filter(:id => {:lt => 5}).to_sql
30
- # => "...WHERE properties.id < 5"
33
+ Property.filter(id: {not_in: [5, 10, 15]}).to_sql
34
+ # => "... WHERE properties.id NOT IN (5, 10, 15) ..."
31
35
 
32
- Property.filter(:id => {:lte => 5}).to_sql
33
- # => "...WHERE properties.id <= 5"
36
+ Property.filter(id: {gt: 5}).to_sql
37
+ Property.filter(id: {greater_than: 5}).to_sql
38
+ # => "... WHERE properties.id > 5 ..."
34
39
 
35
- Property.filter(:address_id => nil).to_sql
36
- # => "...WHERE properties.address_id IS NULL..."
40
+ Property.filter(id: {gte: 5}).to_sql
41
+ Property.filter(id: {gteq: 5}).to_sql
42
+ Property.filter(id: {greater_than_or_equal_to: 5}).to_sql
43
+ # => "... WHERE properties.id >= 5 ..."
37
44
 
38
- Property.filter(:address_id => false).to_sql
39
- # => "...WHERE properties.address_id IS NULL..."
45
+ Property.filter(id: {lt: 5}).to_sql
46
+ Property.filter(id: {less_than: 5}).to_sql
47
+ # => "... WHERE properties.id < 5 ..."
40
48
 
41
- Property.filter(:address_id => true).to_sql
42
- # => "...WHERE properties.address_id IS NOT NULL..."
49
+ Property.filter(id: {lte: 5}).to_sql
50
+ Property.filter(id: {lteq: 5}).to_sql
51
+ Property.filter(id: {less_than_or_equal_to: 5}).to_sql
52
+ # => "... WHERE properties.id <= 5 ..."
53
+
54
+ Property.filter(address_id: nil).to_sql
55
+ # => "... WHERE properties.address_id IS NULL ..."
56
+
57
+ Property.filter(address_id: false).to_sql
58
+ # => "... WHERE properties.address_id IS NULL ..."
59
+
60
+ Property.filter(boolean_column: false).to_sql
61
+ # => "... WHERE properties.boolean_column = FALSE ..."
62
+
63
+ Property.filter(address_id: true).to_sql
64
+ # => "... WHERE properties.address_id IS NOT NULL ..."
65
+
66
+ Property.filter(boolean_column: true).to_sql
67
+ # => "... WHERE properties.boolean_column = TRUE ..."
68
+ ```
69
+
70
+ String columns:
71
+
72
+ ```ruby
73
+ Property.filter(name: {like: 'nam%'}).to_sql
74
+ # => "... WHERE properties.name LIKE 'nam%' ..."
75
+
76
+ Property.filter(name: {ts_match: 'name'}).to_sql
77
+ # => "... WHERE to_tsvector("properties"."name") @@ to_tsquery('name') ..."
43
78
  ```
44
79
 
45
80
  It can also work with array columns:
46
81
 
47
82
  ```ruby
48
- Property.filter(:tags => 'Skyscraper').to_sql
83
+ Property.filter(tags: 'Skyscraper').to_sql
49
84
  # => "...WHERE properties.tags = '{'Skyscraper'}'..."
50
85
 
51
- Property.filter(:tags => ['Skyscraper', 'Brick']).to_sql
52
- # => "...WHERE (properties.tags = '{"Skyscraper", "Brick"}')..."
86
+ Property.filter(tags: ['Skyscraper', 'Brick']).to_sql
87
+ # => "...WHERE properties.tags = '{"Skyscraper", "Brick"}'..."
88
+
89
+ Property.filter(tags: {overlaps: ['Skyscraper', 'Brick']}).to_sql
90
+ # => "...WHERE properties.tags && '{"Skyscraper", "Brick"}'..."
91
+
92
+ Property.filter(tags: {contains: ['Skyscraper', 'Brick']}).to_sql
93
+ # => "...WHERE accounts.tags @> '{"Skyscraper", "Brick"}'..."
53
94
 
54
- Property.filter(:tags => {overlaps: ['Skyscraper', 'Brick']}).to_sql
55
- # => "...WHERE properties.tags && '{"Skyscraper", "Brick"}')..."
95
+ Property.filter(tags: {excludes: ['Skyscraper', 'Brick']}).to_sql
96
+ # => "...WHERE NOT (accounts.tags @> '{"Skyscraper", "Brick"}')..."
56
97
 
57
- Property.filter(:tags => {contains: ['Skyscraper', 'Brick']}).to_sql
58
- # => "...WHERE accounts.tags @> '{"Skyscraper", "Brick"}')..."
98
+ Property.filter(tags: {contained_by: ['Skyscraper', 'Brick']}).to_sql
99
+ # => "...WHERE accounts.tags <@ '{"Skyscraper", "Brick"}'..."
59
100
  ```
60
101
 
61
102
  And JSON columns:
@@ -70,6 +111,12 @@ Property.filter(metadata: { contains: { key: 'value' } }).to_sql
70
111
  Property.filter(metadata: { has_key: 'key' }).to_sql
71
112
  # => "...WHERE "properties"."metadata" ? 'key'..."
72
113
 
114
+ Property.filter(metadata: { has_keys: ['key1', 'key2'] }).to_sql
115
+ # => "...WHERE "properties"."metadata" ?& array['key1', 'key2']..."
116
+
117
+ Property.filter(metadata: { has_any_key: ['key1', 'key2'] }).to_sql
118
+ # => "...WHERE "properties"."metadata" ?| array['key1', 'key2']..."
119
+
73
120
  Property.filter("metadata.key": { eq: 'value' }).to_sql
74
121
  # => "...WHERE "properties"."metadata" #> '{key}' = 'value'..."
75
122
  ```
@@ -77,7 +124,7 @@ Property.filter("metadata.key": { eq: 'value' }).to_sql
77
124
  It can also sort on relations:
78
125
 
79
126
  ```ruby
80
- Photo.filter(:property => {name: 'Empire State'}).to_sql
81
- # => "...INNER JOIN properties ON properties.id = photos.property_id
82
- # => " WHERE properties.name = 'Empire State'"
83
- ```
127
+ Photo.filter(property: {name: 'Empire State'}).to_sql
128
+ # => "... INNER JOIN properties ON properties.id = photos.property_id ...
129
+ # => "... WHERE properties.name = 'Empire State'"
130
+ ```
@@ -4,26 +4,40 @@ require 'arel/extensions'
4
4
  class ActiveRecord::UnkownFilterError < NoMethodError
5
5
  end
6
6
 
7
+ class ActiveRecord::Associations::AliasTracker
8
+
9
+ def initialize(connection, aliases)
10
+ @aliases = aliases
11
+ @connection = connection
12
+ @relation_trail = {}
13
+ end
14
+
15
+ def aliased_table_for_relation(trail, arel_table, &block)
16
+ @relation_trail[trail] ||= aliased_table_for(arel_table, &block)
17
+ end
18
+
19
+ end
20
+
7
21
  module ActiveRecord::Filter
8
22
 
9
23
  delegate :filter, :filter_for, to: :all
10
-
24
+
11
25
  def inherited(subclass)
12
26
  super
13
27
  subclass.instance_variable_set('@filters', HashWithIndifferentAccess.new)
14
28
  end
15
-
29
+
16
30
  def filters
17
31
  @filters
18
32
  end
19
-
33
+
20
34
  def filter_on(name, dependent_joins=nil, &block)
21
35
  @filters[name.to_s] = {
22
36
  joins: dependent_joins,
23
37
  block: block
24
38
  }
25
39
  end
26
-
40
+
27
41
  end
28
42
 
29
43
  module ActiveRecord
@@ -33,7 +47,7 @@ module ActiveRecord
33
47
  custom = []
34
48
  [build_filter_joins(klass, filters, [], custom), custom]
35
49
  end
36
-
50
+
37
51
  def self.build_filter_joins(klass, filters, relations=[], custom=[])
38
52
  if filters.is_a?(Array)
39
53
  filters.each { |f| build_filter_joins(klass, f, relations, custom) }.compact
@@ -41,7 +55,7 @@ module ActiveRecord
41
55
  filters.each do |key, value|
42
56
  if klass.filters.has_key?(key.to_sym)
43
57
  js = klass.filters.dig(key.to_sym, :joins)
44
-
58
+
45
59
  if js.is_a?(Array)
46
60
  js.each do |j|
47
61
  if j.is_a?(String)
@@ -59,13 +73,37 @@ module ActiveRecord
59
73
  end
60
74
  elsif reflection = klass._reflections[key.to_s]
61
75
  if value.is_a?(Hash)
62
- relations << {
63
- key => build_filter_joins(reflection.klass, value, [], custom)
64
- }
76
+ relations << if reflection.polymorphic?
77
+ value = value.dup
78
+ join_klass = value.delete(:as).safe_constantize
79
+ right_table = join_klass.arel_table
80
+ left_table = reflection.active_record.arel_table
81
+
82
+ on = right_table[join_klass.primary_key].
83
+ eq(left_table[reflection.foreign_key]).
84
+ and(left_table[reflection.foreign_type].eq(join_klass.name))
85
+
86
+ cross_boundry_joins = join_klass.left_outer_joins(ActiveRecord::PredicateBuilder.filter_joins(join_klass, value).flatten).send(:build_joins, [])
87
+
88
+ [
89
+ left_table.join(right_table, Arel::Nodes::OuterJoin).on(on).join_sources,
90
+ cross_boundry_joins
91
+ ]
92
+ else
93
+ {
94
+ key => build_filter_joins(reflection.klass, value, [], custom)
95
+ }
96
+ end
97
+ elsif value.is_a?(Array)
98
+ value.each do |v|
99
+ relations << {
100
+ key => build_filter_joins(reflection.klass, v, [], custom)
101
+ }
102
+ end
65
103
  elsif value != true && value != false && value != 'true' && value != 'false' && !value.nil?
66
104
  relations << key
67
105
  end
68
- elsif !klass.columns_hash.has_key?(key.to_s) && key.to_s.ends_with?('_ids') && reflection = klass._reflections[key.to_s.gsub(/_ids$/, 's')]
106
+ elsif !klass.columns_hash.has_key?(key.to_s) && key.to_s.end_with?('_ids') && reflection = klass._reflections[key.to_s.gsub(/_ids$/, 's')]
69
107
  relations << reflection.name
70
108
  elsif reflection = klass.reflect_on_all_associations(:has_and_belongs_to_many).find {|r| r.join_table == key.to_s && value.keys.first.to_s == r.association_foreign_key.to_s }
71
109
  reflection = klass._reflections[klass._reflections[reflection.name.to_s].send(:delegate_reflection).options[:through].to_s]
@@ -73,17 +111,17 @@ module ActiveRecord
73
111
  end
74
112
  end
75
113
  end
76
-
114
+
77
115
  relations
78
116
  end
79
-
80
- def build_from_filter_hash(attributes)
117
+
118
+ def build_from_filter_hash(attributes, relation_trail, alias_tracker)
81
119
  if attributes.is_a?(Array)
82
- node = build_from_filter_hash(attributes.shift)
120
+ node = build_from_filter_hash(attributes.shift, relation_trail, alias_tracker)
83
121
 
84
122
  n = attributes.shift(2)
85
123
  while !n.empty?
86
- n[1] = build_from_filter_hash(n[1])
124
+ n[1] = build_from_filter_hash(n[1], relation_trail, alias_tracker)
87
125
  if n[0] == 'AND'
88
126
  if node.is_a?(Arel::Nodes::And)
89
127
  node.children.push(n[1])
@@ -92,39 +130,46 @@ module ActiveRecord
92
130
  end
93
131
  elsif n[0] == 'OR'
94
132
  node = Arel::Nodes::Grouping.new(node).or(Arel::Nodes::Grouping.new(n[1]))
133
+ elsif !n[0].is_a?(String)
134
+ n[0] = build_from_filter_hash(n[0], relation_trail, alias_tracker)
135
+ if node.is_a?(Arel::Nodes::And)
136
+ node.children.push(n[0])
137
+ else
138
+ node = node.and(n[0])
139
+ end
95
140
  else
96
141
  raise 'lll'
97
142
  end
98
143
  n = attributes.shift(2)
99
144
  end
100
-
145
+
101
146
  node
102
147
  elsif attributes.is_a?(Hash)
103
- expand_from_filter_hash(attributes)
148
+ expand_from_filter_hash(attributes, relation_trail, alias_tracker)
104
149
  else
105
- expand_from_filter_hash({id: attributes})
150
+ expand_from_filter_hash({id: attributes}, relation_trail, alias_tracker)
106
151
  end
107
152
  end
108
-
109
- def expand_from_filter_hash(attributes)
153
+
154
+ def expand_from_filter_hash(attributes, relation_trail, alias_tracker)
110
155
  klass = table.send(:klass)
111
-
156
+
112
157
  children = attributes.flat_map do |key, value|
113
158
  if custom_filter = klass.filters[key]
114
- self.instance_exec(klass, table, key, value, &custom_filter[:block])
159
+ self.instance_exec(klass, table, key, value, relation_trail, alias_tracker, &custom_filter[:block])
115
160
  elsif column = klass.columns_hash[key.to_s] || klass.columns_hash[key.to_s.split('.').first]
116
- expand_filter_for_column(key, column, value)
161
+ expand_filter_for_column(key, column, value, relation_trail)
117
162
  elsif relation = klass.reflect_on_association(key)
118
- expand_filter_for_relationship(relation, value)
119
- elsif key.to_s.ends_with?('_ids') && relation = klass.reflect_on_association(key.to_s.gsub(/_ids$/, 's'))
120
- expand_filter_for_relationship(relation, {id: value})
163
+ expand_filter_for_relationship(relation, value, relation_trail, alias_tracker)
164
+ elsif key.to_s.end_with?('_ids') && relation = klass.reflect_on_association(key.to_s.gsub(/_ids$/, 's'))
165
+ expand_filter_for_relationship(relation, {id: value}, relation_trail, alias_tracker)
121
166
  elsif relation = klass.reflect_on_all_associations(:has_and_belongs_to_many).find {|r| r.join_table == key.to_s && value.keys.first.to_s == r.association_foreign_key.to_s }
122
- expand_filter_for_join_table(relation, value)
167
+ expand_filter_for_join_table(relation, value, relation_trail, alias_tracker)
123
168
  else
124
169
  raise ActiveRecord::UnkownFilterError.new("Unkown filter \"#{key}\" for #{klass}.")
125
170
  end
126
171
  end
127
-
172
+
128
173
  children.compact!
129
174
  if children.size > 1
130
175
  Arel::Nodes::And.new(children)
@@ -132,20 +177,11 @@ module ActiveRecord
132
177
  children.first
133
178
  end
134
179
  end
135
-
136
- def expand_filter_for_column(key, column, value)
137
- # Not sure why
138
- # activerecord/lib/active_record/table_metadata.rb#arel_attribute
139
- # doesn't work here, something's not working with a
140
- # Arel::Nodes::TableAlias, would like to go back to it one day
141
- attribute = if klass = table.send(:klass)
142
- if Arel::Nodes::TableAlias === table.send(:arel_table)
143
- klass.arel_attribute(column.name, table.send(:arel_table).left)
144
- else
145
- klass.arel_attribute(column.name, table.send(:arel_table))
146
- end
147
- else
148
- table.send(:arel_table)[column.name]
180
+
181
+ def expand_filter_for_column(key, column, value, relation_trail)
182
+ attribute = table.arel_table[column.name]
183
+ relation_trail.each do |rt|
184
+ attribute = Arel::Attributes::Relation.new(attribute, rt)
149
185
  end
150
186
 
151
187
  if column.type == :json || column.type == :jsonb
@@ -153,7 +189,7 @@ module ActiveRecord
153
189
  names.shift
154
190
  attribute = attribute.dig(names)
155
191
  end
156
-
192
+
157
193
  if value.is_a?(Hash)
158
194
  nodes = value.map do |subkey, subvalue|
159
195
  expand_filter_for_arel_attribute(column, attribute, subkey, subvalue)
@@ -173,19 +209,19 @@ module ActiveRecord
173
209
  else
174
210
  raise ActiveRecord::UnkownFilterError.new("Unkown type for #{column}. (type #{value.class})")
175
211
  end
176
-
212
+
177
213
  end
178
-
214
+
179
215
  def expand_filter_for_arel_attribute(column, attribute, key, value)
180
216
  case key.to_sym
181
217
  when :contains
182
- attribute.contains(column.array ? Array(value) : value)
218
+ attribute.contains(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
183
219
  when :contained_by
184
- attribute.contained_by(column.array ? Array(value) : value)
220
+ attribute.contained_by(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
185
221
  when :equal_to, :eq
186
222
  attribute.eq(value)
187
223
  when :excludes
188
- attribute.excludes(Array(value))
224
+ attribute.excludes(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
189
225
  when :greater_than, :gt
190
226
  attribute.gt(value)
191
227
  when :greater_than_or_equal_to, :gteq, :gte
@@ -206,7 +242,7 @@ module ActiveRecord
206
242
  # elsif # EWKT
207
243
  # elsif # WKT
208
244
  # end
209
-
245
+
210
246
  # TODO us above if to determin if SRID sent
211
247
  geometry_value = if value.is_a?(Hash)
212
248
  Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromGeoJSON', [Arel::Nodes.build_quoted(JSON.generate(subvalue))]), 4326])
@@ -228,7 +264,7 @@ module ActiveRecord
228
264
  when :not_in
229
265
  attribute.not_in(value)
230
266
  when :overlaps
231
- attribute.overlaps(value)
267
+ attribute.overlaps(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
232
268
  when :not_overlaps
233
269
  attribute.not_overlaps(value)
234
270
  when :ts_match
@@ -250,45 +286,67 @@ module ActiveRecord
250
286
  raise "Not Supported value for within: #{value.inspect}"
251
287
  end
252
288
  else
253
- raise "Not Supported: #{key.to_sym}"
289
+ raise "Not Supported: #{key.to_sym} on column \"#{column.name}\" of type #{column.type}"
254
290
  end
255
291
  end
256
-
257
- def expand_filter_for_relationship(relation, value)
292
+
293
+ def expand_filter_for_relationship(relation, value, relation_trail, alias_tracker)
258
294
  case relation.macro
259
295
  when :has_many
260
296
  if value == true || value == 'true'
261
297
  counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
262
298
  if relation.active_record.column_names.include?(counter_cache_column_name.to_s)
263
- return table.arel_attribute(counter_cache_column_name.to_sym).gt(0)
299
+ return table.arel_table[counter_cache_column_name.to_sym].gt(0)
264
300
  else
265
301
  raise "Not Supported: #{relation.name}"
266
302
  end
267
303
  elsif value == false || value == 'false'
268
304
  counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
269
305
  if relation.active_record.column_names.include?(counter_cache_column_name.to_s)
270
- return table.arel_attribute(counter_cache_column_name.to_sym).eq(0)
306
+ return table.arel_table[counter_cache_column_name.to_sym].eq(0)
271
307
  else
272
308
  raise "Not Supported: #{relation.name}"
273
309
  end
274
310
  end
311
+
275
312
  when :belongs_to
276
313
  if value == true || value == 'true'
277
- return table.arel_attribute(relation.foreign_key).not_eq(nil)
314
+ return table.arel_table[relation.foreign_key].not_eq(nil)
278
315
  elsif value == false || value == 'false' || value.nil?
279
- return table.arel_attribute(relation.foreign_key).eq(nil)
316
+ return table.arel_table[relation.foreign_key].eq(nil)
280
317
  end
281
318
  end
282
-
283
- builder = associated_predicate_builder(relation.name.to_sym)
284
- builder.build_from_filter_hash(value)
319
+
320
+ if relation.polymorphic?
321
+ value = value.dup
322
+ klass = value.delete(:as).safe_constantize
323
+
324
+ builder = self.class.new(TableMetadata.new(
325
+ klass,
326
+ alias_tracker.aliased_table_for_relation(relation_trail + ["#{klass.table_name}_as_#{relation.name}"], klass.arel_table) { klass.arel_table.name },
327
+ relation
328
+ ))
329
+ builder.build_from_filter_hash(value, relation_trail + ["#{klass.table_name}_as_#{relation.name}"], alias_tracker)
330
+ else
331
+ builder = self.class.new(TableMetadata.new(
332
+ relation.klass,
333
+ alias_tracker.aliased_table_for_relation(relation_trail + [relation.name], relation.klass.arel_table) { relation.alias_candidate(table.arel_table.name || relation.klass.arel_table) },
334
+ relation
335
+ ))
336
+ builder.build_from_filter_hash(value, relation_trail + [relation.name], alias_tracker)
337
+ end
338
+
285
339
  end
286
-
287
- def expand_filter_for_join_table(relation, value)
288
- relation = relation.active_record._reflections[relation.active_record._reflections[relation.name.to_s].send(:delegate_reflection).options[:through].to_s]
289
340
 
290
- builder = associated_predicate_builder(relation.name.to_sym)
291
- builder.build_from_filter_hash(value)
341
+
342
+ def expand_filter_for_join_table(relation, value, relation_trail, alias_tracker)
343
+ relation = relation.active_record._reflections[relation.active_record._reflections[relation.name.to_s].send(:delegate_reflection).options[:through].to_s]
344
+ builder = self.class.new(TableMetadata.new(
345
+ relation.klass,
346
+ alias_tracker.aliased_table_for_relation(relation_trail + [relation.name], relation.klass.arel_table) { relation.alias_candidate(table.arel_table.name || relation.klass.arel_table) },
347
+ relation
348
+ ))
349
+ builder.build_from_filter_hash(value, relation_trail + [relation.name], alias_tracker)
292
350
  end
293
351
 
294
352
  end
@@ -303,18 +361,13 @@ module ActiveRecord
303
361
  @predicate_builder = predicate_builder
304
362
  end
305
363
 
306
- def build(filters)
364
+ def build(filters, alias_tracker)
307
365
  if filters.is_a?(Hash) || filters.is_a?(Array)
308
- # attributes = predicate_builder.resolve_column_aliases(filters)
309
- # attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes)
310
- # attributes.stringify_keys!
311
- #
312
- # attributes, binds = predicate_builder.create_binds(attributes)
313
- parts = [predicate_builder.build_from_filter_hash(filters)]
366
+ parts = [predicate_builder.build_from_filter_hash(filters, [], alias_tracker)]
314
367
  else
315
368
  raise ArgumentError, "Unsupported argument type: #{filters.inspect} (#{filters.class})"
316
369
  end
317
-
370
+
318
371
  WhereClause.new(parts)
319
372
  end
320
373
 
@@ -332,12 +385,12 @@ class ActiveRecord::Relation
332
385
  @filters = []
333
386
  super
334
387
  end
335
-
388
+
336
389
  def initialize_copy(other)
337
390
  @filters = @filters.deep_dup
338
391
  super
339
392
  end
340
-
393
+
341
394
  def clean_filters(value)
342
395
  if value.class.name == 'ActionController::Parameters'.freeze
343
396
  value.to_unsafe_h
@@ -350,34 +403,43 @@ class ActiveRecord::Relation
350
403
 
351
404
  def filter(filters)
352
405
  filters = clean_filters(filters)
353
-
406
+
354
407
  if filters.nil? || filters.empty?
355
408
  self
356
409
  else
357
410
  spawn.filter!(filters)
358
411
  end
359
412
  end
360
-
413
+
361
414
  def filter!(filters)
362
415
  js = ActiveRecord::PredicateBuilder.filter_joins(klass, filters)
363
- js.each { |j| joins!(j) if j.present? }
416
+ js.flatten.each do |j|
417
+ if j.is_a?(String)
418
+ joins!(j)
419
+ elsif j.is_a?(Arel::Nodes::Join)
420
+ joins!(j)
421
+ elsif j.present?
422
+ left_outer_joins!(j)
423
+ end
424
+ end
364
425
  @filters << filters
365
426
  self
366
427
  end
367
-
428
+
368
429
  def filter_clause_factory
369
430
  @filter_clause_factory ||= FilterClauseFactory.new(klass, predicate_builder)
370
431
  end
371
-
432
+
372
433
  def build_arel(aliases)
373
434
  arel = super
374
- build_filters(arel)
435
+ my_alias_tracker = ActiveRecord::Associations::AliasTracker.create(connection, table.name, [])
436
+ build_filters(arel, my_alias_tracker)
375
437
  arel
376
438
  end
377
-
378
- def build_filters(manager)
439
+
440
+ def build_filters(manager, alias_tracker)
379
441
  @filters.each do |filters|
380
- manager.where(filter_clause_factory.build(filters).ast)
442
+ manager.where(filter_clause_factory.build(filters, alias_tracker).ast)
381
443
  end
382
444
  end
383
445
 
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Filter
3
- VERSION = '6.0.0.3'
3
+ VERSION = '6.1.0.rc1'
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-filter
3
3
  version: !ruby/object:Gem::Version
4
- version: 6.0.0.3
4
+ version: 6.1.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-08-16 00:00:00.000000000 Z
11
+ date: 2020-12-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 6.0.0
19
+ version: 6.1.0
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 6.0.0
26
+ version: 6.1.0
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: arel-extensions
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 6.0.0
33
+ version: 6.1.0.rc2
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: 6.0.0
40
+ version: 6.1.0.rc2
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: pg
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +58,14 @@ dependencies:
58
58
  requirements:
59
59
  - - ">="
60
60
  - !ruby/object:Gem::Version
61
- version: 6.0.0
61
+ version: 6.1.0
62
62
  type: :development
63
63
  prerelease: false
64
64
  version_requirements: !ruby/object:Gem::Requirement
65
65
  requirements:
66
66
  - - ">="
67
67
  - !ruby/object:Gem::Version
68
- version: 6.0.0
68
+ version: 6.1.0
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: bundler
71
71
  requirement: !ruby/object:Gem::Requirement
@@ -142,14 +142,14 @@ dependencies:
142
142
  requirements:
143
143
  - - ">="
144
144
  - !ruby/object:Gem::Version
145
- version: 6.0.0
145
+ version: 6.1.0
146
146
  type: :development
147
147
  prerelease: false
148
148
  version_requirements: !ruby/object:Gem::Requirement
149
149
  requirements:
150
150
  - - ">="
151
151
  - !ruby/object:Gem::Version
152
- version: 6.0.0
152
+ version: 6.1.0
153
153
  - !ruby/object:Gem::Dependency
154
154
  name: faker
155
155
  requirement: !ruby/object:Gem::Requirement
@@ -194,7 +194,7 @@ homepage: https://github.com/malomalo/activerecord-filter
194
194
  licenses:
195
195
  - MIT
196
196
  metadata: {}
197
- post_install_message:
197
+ post_install_message:
198
198
  rdoc_options:
199
199
  - "--main"
200
200
  - README.md
@@ -207,12 +207,12 @@ required_ruby_version: !ruby/object:Gem::Requirement
207
207
  version: '0'
208
208
  required_rubygems_version: !ruby/object:Gem::Requirement
209
209
  requirements:
210
- - - ">="
210
+ - - ">"
211
211
  - !ruby/object:Gem::Version
212
- version: '0'
212
+ version: 1.3.1
213
213
  requirements: []
214
- rubygems_version: 3.0.3
215
- signing_key:
214
+ rubygems_version: 3.1.4
215
+ signing_key:
216
216
  specification_version: 4
217
217
  summary: A safe way to accept user parameters and query against your ActiveRecord
218
218
  Models