activerecord-filter 7.0.0 → 7.0.1

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: 4d1644f69a43b55b39739e14afa9e43d907dde5046cd59f82d6b1984c03bc585
4
- data.tar.gz: 21df3a894fb089c20894fde8b6119b6567bd1c3424b0147ef9aebe34b35356ac
3
+ metadata.gz: 48643ae6cc991c1dff7397b4d72c5f2f0c125809f06c03deda47557f9f5102d8
4
+ data.tar.gz: adcc3b342d478ad0802882041abf9ef583abd552d35d8ffccb574987f66d7a4b
5
5
  SHA512:
6
- metadata.gz: cba77ac497ae138c663cbd7b797e81fe6f1ca8ca05b869f4a8c7e8d8c5c9321018623b5c611272b59bb5439c897d7c7363fb7f2a7169af7d65af29193171c72d
7
- data.tar.gz: 892989bcf29310cbb657f22642e9f37c079c7d3f0f87e22a6e288e0ba2bd98ebb8179f87e49de3f07820b91a725d48fa28b0a50a8f31c8e791bdc987c2355b3e
6
+ metadata.gz: 464eae805db85247060f6553cf242a49f852ff5d04e4aed17d3613e6a155e4dbaa49c72dcca89c14449fa2aee70ee15ef39afefdce6ed29f18d4c6141e31c8f3
7
+ data.tar.gz: 116e8b496f68e08f8b6c350432cd806de28b7a830898c84a02c0b4da4d6968d42c2da870e585eeeb247116592ee3db66189f078b88f5cacdd777633971e73b5b
@@ -0,0 +1,12 @@
1
+ module ActiveRecord::Filter::AliasTrackerExtension
2
+
3
+ def initialize(*, **)
4
+ super
5
+ @relation_trail = {}
6
+ end
7
+
8
+ def aliased_table_for_relation(trail, arel_table, &block)
9
+ @relation_trail[trail] ||= aliased_table_for(arel_table, &block)
10
+ end
11
+
12
+ end
@@ -0,0 +1,22 @@
1
+ class ActiveRecord::Filter::FilterClauseFactory
2
+
3
+ def initialize(klass, predicate_builder)
4
+ @klass = klass
5
+ @predicate_builder = predicate_builder
6
+ end
7
+
8
+ def build(filters, alias_tracker)
9
+ if filters.is_a?(Hash) || filters.is_a?(Array)
10
+ parts = [predicate_builder.build_from_filter_hash(filters, [], alias_tracker)]
11
+ else
12
+ raise ArgumentError, "Unsupported argument type: #{filters.inspect} (#{filters.class})"
13
+ end
14
+
15
+ ActiveRecord::Relation::WhereClause.new(parts)
16
+ end
17
+
18
+ protected
19
+
20
+ attr_reader :klass, :predicate_builder
21
+
22
+ end
@@ -0,0 +1,328 @@
1
+ require "active_support/concern"
2
+
3
+ module ActiveRecord::Filter::PredicateBuilderExtension
4
+
5
+ extend ActiveSupport::Concern
6
+
7
+ class_methods do
8
+ def filter_joins(klass, filters)
9
+ custom = []
10
+ [build_filter_joins(klass, filters, [], custom), custom]
11
+ end
12
+
13
+ def build_filter_joins(klass, filters, relations=[], custom=[])
14
+ if filters.is_a?(Array)
15
+ filters.each { |f| build_filter_joins(klass, f, relations, custom) }.compact
16
+ elsif filters.is_a?(Hash)
17
+ filters.each do |key, value|
18
+ if klass.filters.has_key?(key.to_sym)
19
+ js = klass.filters.dig(key.to_sym, :joins)
20
+
21
+ if js.is_a?(Array)
22
+ js.each do |j|
23
+ if j.is_a?(String)
24
+ custom << j
25
+ else
26
+ relations << j
27
+ end
28
+ end
29
+ elsif js
30
+ if js.is_a?(String)
31
+ custom << js
32
+ else
33
+ relations << js
34
+ end
35
+ end
36
+ elsif reflection = klass._reflections[key.to_s]
37
+ if value.is_a?(Hash)
38
+ relations << if reflection.polymorphic?
39
+ value = value.dup
40
+ join_klass = value.delete(:as).safe_constantize
41
+ right_table = join_klass.arel_table
42
+ left_table = reflection.active_record.arel_table
43
+
44
+ on = right_table[join_klass.primary_key].
45
+ eq(left_table[reflection.foreign_key]).
46
+ and(left_table[reflection.foreign_type].eq(join_klass.name))
47
+
48
+ cross_boundry_joins = join_klass.left_outer_joins(ActiveRecord::PredicateBuilder.filter_joins(join_klass, value).flatten).send(:build_joins, [])
49
+
50
+ [
51
+ left_table.join(right_table, Arel::Nodes::OuterJoin).on(on).join_sources,
52
+ cross_boundry_joins
53
+ ]
54
+ else
55
+ {
56
+ key => build_filter_joins(reflection.klass, value, [], custom)
57
+ }
58
+ end
59
+ elsif value.is_a?(Array)
60
+ value.each do |v|
61
+ relations << {
62
+ key => build_filter_joins(reflection.klass, v, [], custom)
63
+ }
64
+ end
65
+ elsif value != true && value != false && value != 'true' && value != 'false' && !value.nil?
66
+ relations << key
67
+ end
68
+ 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
+ relations << reflection.name
70
+ 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
+ reflection = klass._reflections[klass._reflections[reflection.name.to_s].send(:delegate_reflection).options[:through].to_s]
72
+ relations << {reflection.name => build_filter_joins(reflection.klass, value)}
73
+ else
74
+ {key => value}
75
+ end
76
+ end
77
+ end
78
+
79
+ relations
80
+ end
81
+ end
82
+
83
+ def build_from_filter_hash(attributes, relation_trail, alias_tracker)
84
+ if attributes.is_a?(Array)
85
+ node = build_from_filter_hash(attributes.shift, relation_trail, alias_tracker)
86
+
87
+ n = attributes.shift(2)
88
+ while !n.empty?
89
+ n[1] = build_from_filter_hash(n[1], relation_trail, alias_tracker)
90
+ if n[0] == 'AND'
91
+ if node.is_a?(Arel::Nodes::And)
92
+ node.children.push(n[1])
93
+ else
94
+ node = node.and(n[1])
95
+ end
96
+ elsif n[0] == 'OR'
97
+ node = Arel::Nodes::Grouping.new(node).or(Arel::Nodes::Grouping.new(n[1]))
98
+ elsif !n[0].is_a?(String)
99
+ n[0] = build_from_filter_hash(n[0], relation_trail, alias_tracker)
100
+ if node.is_a?(Arel::Nodes::And)
101
+ node.children.push(n[0])
102
+ else
103
+ node = node.and(n[0])
104
+ end
105
+ else
106
+ raise 'lll'
107
+ end
108
+ n = attributes.shift(2)
109
+ end
110
+
111
+ node
112
+ elsif attributes.is_a?(Hash)
113
+ expand_from_filter_hash(attributes, relation_trail, alias_tracker)
114
+ else
115
+ expand_from_filter_hash({id: attributes}, relation_trail, alias_tracker)
116
+ end
117
+ end
118
+
119
+ def expand_from_filter_hash(attributes, relation_trail, alias_tracker)
120
+ klass = table.send(:klass)
121
+
122
+ children = attributes.flat_map do |key, value|
123
+ if custom_filter = klass.filters[key]
124
+ self.instance_exec(klass, table, key, value, relation_trail, alias_tracker, &custom_filter[:block])
125
+ elsif column = klass.columns_hash[key.to_s] || klass.columns_hash[key.to_s.split('.').first]
126
+ expand_filter_for_column(key, column, value, relation_trail)
127
+ elsif relation = klass.reflect_on_association(key)
128
+ expand_filter_for_relationship(relation, value, relation_trail, alias_tracker)
129
+ elsif key.to_s.end_with?('_ids') && relation = klass.reflect_on_association(key.to_s.gsub(/_ids$/, 's'))
130
+ expand_filter_for_relationship(relation, {id: value}, relation_trail, alias_tracker)
131
+ 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 }
132
+ expand_filter_for_join_table(relation, value, relation_trail, alias_tracker)
133
+ else
134
+ raise ActiveRecord::UnkownFilterError.new("Unkown filter \"#{key}\" for #{klass}.")
135
+ end
136
+ end
137
+
138
+ children.compact!
139
+ if children.size > 1
140
+ Arel::Nodes::And.new(children)
141
+ else
142
+ children.first
143
+ end
144
+ end
145
+
146
+ def expand_filter_for_column(key, column, value, relation_trail)
147
+ attribute = table.arel_table[column.name]
148
+ relation_trail.each do |rt|
149
+ attribute = Arel::Attributes::Relation.new(attribute, rt)
150
+ end
151
+
152
+ if column.type == :json || column.type == :jsonb
153
+ names = key.to_s.split('.')
154
+ names.shift
155
+ attribute = attribute.dig(names)
156
+ elsif column.type == :geometry
157
+ value = if value.is_a?(Hash)
158
+ value.transform_values { |v| geometry_from_value(v) }
159
+ else
160
+ geometry_from_value(value)
161
+ end
162
+ end
163
+
164
+ if value.is_a?(Hash)
165
+ nodes = value.map do |subkey, subvalue|
166
+ expand_filter_for_arel_attribute(column, attribute, subkey, subvalue)
167
+ end
168
+ nodes.inject { |c, n| c.nil? ? n : c.and(n) }
169
+ elsif value == nil
170
+ attribute.eq(nil)
171
+ elsif value == true || value == 'true'
172
+ column.type == :boolean ? attribute.eq(true) : attribute.not_eq(nil)
173
+ elsif value == false || value == 'false'
174
+ column.type == :boolean ? attribute.eq(false) : attribute.eq(nil)
175
+ elsif value.is_a?(Array) && !column.array
176
+ attribute.in(value)
177
+ elsif column.type != :json && column.type != :jsonb
178
+ converted_value = column.array ? Array(value) : value
179
+ attribute.eq(converted_value)
180
+ else
181
+ raise ActiveRecord::UnkownFilterError.new("Unkown type for #{column}. (type #{value.class})")
182
+ end
183
+
184
+ end
185
+
186
+ # TODO determine if SRID sent and cast to correct SRID
187
+ def geometry_from_value(value)
188
+ if value.is_a?(Array)
189
+ value.map { |g| geometry_from_value(g) }
190
+ elsif value.is_a?(Hash)
191
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromGeoJSON', [Arel::Nodes.build_quoted(JSON.generate(value))]), 4326])
192
+ elsif value[0,1] == "\x00" || value[0,1] == "\x01"
193
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromEWKB', [Arel::Nodes::BinaryValue.new(value)]), 4326])
194
+ elsif value[0,4] =~ /[0-9a-fA-F]{4}/
195
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromEWKB', [Arel::Nodes::HexEncodedBinaryValue.new(value)]), 4326])
196
+ else
197
+ Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromText', [Arel::Nodes.build_quoted(value)]), 4326])
198
+ end
199
+ end
200
+
201
+ def expand_filter_for_arel_attribute(column, attribute, key, value)
202
+ case key.to_sym
203
+ when :contains
204
+ case column.type
205
+ when :geometry
206
+ Arel::Nodes::NamedFunction.new('ST_Contains', [attribute, value])
207
+ else
208
+ attribute.contains(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
209
+ end
210
+ when :contained_by
211
+ attribute.contained_by(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
212
+ when :equal_to, :eq
213
+ case column.type
214
+ when :geometry
215
+ Arel::Nodes::NamedFunction.new('ST_Equals', [attribute, value])
216
+ else
217
+ attribute.eq(value)
218
+ end
219
+ when :excludes
220
+ attribute.excludes(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
221
+ when :greater_than, :gt
222
+ attribute.gt(value)
223
+ when :greater_than_or_equal_to, :gteq, :gte
224
+ attribute.gteq(value)
225
+ when :has_key
226
+ attribute.has_key(value)
227
+ when :has_keys
228
+ attribute.has_keys(*Array(value).map { |x| Arel::Nodes.build_quoted(x) })
229
+ when :has_any_key
230
+ attribute.has_any_key(*Array(value).map { |x| Arel::Nodes.build_quoted(x) })
231
+ when :in
232
+ attribute.in(value)
233
+ when :intersects
234
+ attribute.intersects(value)
235
+ when :less_than, :lt
236
+ attribute.lt(value)
237
+ when :less_than_or_equal_to, :lteq, :lte
238
+ attribute.lteq(value)
239
+ when :like
240
+ attribute.matches(value, nil, true)
241
+ when :ilike
242
+ attribute.matches(value, nil, false)
243
+ when :not, :not_equal, :neq
244
+ attribute.not_eq(value)
245
+ when :not_in
246
+ attribute.not_in(value)
247
+ when :overlaps
248
+ case column.type
249
+ in :geometry
250
+ attribute.overlaps(value)
251
+ else
252
+ attribute.overlaps(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
253
+ end
254
+ when :not_overlaps
255
+ attribute.not_overlaps(value)
256
+ when :ts_match
257
+ if value.is_a?(Array)
258
+ attribute.ts_query(*value)
259
+ else
260
+ attribute.ts_query(value)
261
+ end
262
+ when :within
263
+ attribute.within(Arel::Nodes.build_quoted(value))
264
+ else
265
+ raise "Not Supported: #{key.to_sym} on column \"#{column.name}\" of type #{column.type}"
266
+ end
267
+ end
268
+
269
+ def expand_filter_for_relationship(relation, value, relation_trail, alias_tracker)
270
+ case relation.macro
271
+ when :has_many
272
+ if value == true || value == 'true'
273
+ counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
274
+ if relation.active_record.column_names.include?(counter_cache_column_name.to_s)
275
+ return table.arel_table[counter_cache_column_name.to_sym].gt(0)
276
+ else
277
+ raise "Not Supported: #{relation.name}"
278
+ end
279
+ elsif value == false || value == 'false'
280
+ counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
281
+ if relation.active_record.column_names.include?(counter_cache_column_name.to_s)
282
+ return table.arel_table[counter_cache_column_name.to_sym].eq(0)
283
+ else
284
+ raise "Not Supported: #{relation.name}"
285
+ end
286
+ end
287
+
288
+ when :belongs_to
289
+ if value == true || value == 'true'
290
+ return table.arel_table[relation.foreign_key].not_eq(nil)
291
+ elsif value == false || value == 'false' || value.nil?
292
+ return table.arel_table[relation.foreign_key].eq(nil)
293
+ end
294
+ end
295
+
296
+ if relation.polymorphic?
297
+ value = value.dup
298
+ klass = value.delete(:as).safe_constantize
299
+
300
+ builder = self.class.new(ActiveRecord::TableMetadata.new(
301
+ klass,
302
+ alias_tracker.aliased_table_for_relation(relation_trail + ["#{klass.table_name}_as_#{relation.name}"], klass.arel_table) { klass.arel_table.name },
303
+ relation
304
+ ))
305
+ builder.build_from_filter_hash(value, relation_trail + ["#{klass.table_name}_as_#{relation.name}"], alias_tracker)
306
+ else
307
+ builder = self.class.new(ActiveRecord::TableMetadata.new(
308
+ relation.klass,
309
+ 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) },
310
+ relation
311
+ ))
312
+ builder.build_from_filter_hash(value, relation_trail + [relation.name], alias_tracker)
313
+ end
314
+
315
+ end
316
+
317
+
318
+ def expand_filter_for_join_table(relation, value, relation_trail, alias_tracker)
319
+ relation = relation.active_record._reflections[relation.active_record._reflections[relation.name.to_s].send(:delegate_reflection).options[:through].to_s]
320
+ builder = self.class.new(ActiveRecord::TableMetadata.new(
321
+ relation.klass,
322
+ 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) },
323
+ relation
324
+ ))
325
+ builder.build_from_filter_hash(value, relation_trail + [relation.name], alias_tracker)
326
+ end
327
+
328
+ end
@@ -0,0 +1,65 @@
1
+ module ActiveRecord::Filter::QueryMethodsExtension
2
+ private
3
+ def build_join_buckets
4
+ buckets = Hash.new { |h, k| h[k] = [] }
5
+
6
+ unless left_outer_joins_values.empty?
7
+ stashed_left_joins = []
8
+ left_joins = select_named_joins(left_outer_joins_values, stashed_left_joins) do |left_join|
9
+ if left_join.is_a?(ActiveRecord::QueryMethods::CTEJoin)
10
+ buckets[:join_node] << build_with_join_node(left_join.name, Arel::Nodes::OuterJoin)
11
+ # Add this elsif becasuse PR https://github.com/rails/rails/pull/46843
12
+ # Changed a line https://github.com/rails/rails/blob/ae2983a75ca658d84afa414dea8eaf1cca87aa23/activerecord/lib/active_record/relation/query_methods.rb#L1769
13
+ # that was probably a bug beforehand but allowed nodes to be joined
14
+ # which I think was and still is supported?
15
+ elsif left_join.is_a?(Arel::Nodes::OuterJoin)
16
+ buckets[:join_node] << left_join
17
+ else
18
+ raise ArgumentError, "only Hash, Symbol and Array are allowed"
19
+ end
20
+ end
21
+
22
+ if joins_values.empty?
23
+ buckets[:named_join] = left_joins
24
+ buckets[:stashed_join] = stashed_left_joins
25
+ return buckets, Arel::Nodes::OuterJoin
26
+ else
27
+ stashed_left_joins.unshift construct_join_dependency(left_joins, Arel::Nodes::OuterJoin)
28
+ end
29
+ end
30
+
31
+ joins = joins_values.dup
32
+ if joins.last.is_a?(ActiveRecord::Associations::JoinDependency)
33
+ stashed_eager_load = joins.pop if joins.last.base_klass == klass
34
+ end
35
+
36
+ joins.each_with_index do |join, i|
37
+ joins[i] = Arel::Nodes::StringJoin.new(Arel.sql(join.strip)) if join.is_a?(String)
38
+ end
39
+
40
+ while joins.first.is_a?(Arel::Nodes::Join)
41
+ join_node = joins.shift
42
+ if !join_node.is_a?(Arel::Nodes::LeadingJoin) && (stashed_eager_load || stashed_left_joins)
43
+ buckets[:join_node] << join_node
44
+ else
45
+ buckets[:leading_join] << join_node
46
+ end
47
+ end
48
+
49
+ buckets[:named_join] = select_named_joins(joins, buckets[:stashed_join]) do |join|
50
+ if join.is_a?(Arel::Nodes::Join)
51
+ buckets[:join_node] << join
52
+ elsif join.is_a?(CTEJoin)
53
+ buckets[:join_node] << build_with_join_node(join.name)
54
+ else
55
+ raise "unknown class: %s" % join.class.name
56
+ end
57
+ end
58
+
59
+ buckets[:stashed_join].concat stashed_left_joins if stashed_left_joins
60
+ buckets[:stashed_join] << stashed_eager_load if stashed_eager_load
61
+
62
+ return buckets, Arel::Nodes::InnerJoin
63
+ end
64
+
65
+ end
@@ -0,0 +1,65 @@
1
+ module ActiveRecord::Filter::RelationExtension
2
+
3
+ def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
4
+ @filters = []
5
+ super
6
+ end
7
+
8
+ def initialize_copy(other)
9
+ @filters = @filters.deep_dup
10
+ super
11
+ end
12
+
13
+ def clean_filters(value)
14
+ if value.class.name == 'ActionController::Parameters'.freeze
15
+ value.to_unsafe_h
16
+ elsif value.is_a?(Array)
17
+ value.map { |v| clean_filters(v) }
18
+ else
19
+ value
20
+ end
21
+ end
22
+
23
+ def filter(filters)
24
+ filters = clean_filters(filters)
25
+
26
+ if filters.nil? || filters.empty?
27
+ self
28
+ else
29
+ spawn.filter!(filters)
30
+ end
31
+ end
32
+
33
+ def filter!(filters)
34
+ js = ActiveRecord::PredicateBuilder.filter_joins(klass, filters)
35
+ js.flatten.each do |j|
36
+ if j.is_a?(String)
37
+ joins!(j)
38
+ elsif j.is_a?(Arel::Nodes::Join)
39
+ joins!(j)
40
+ elsif j.present?
41
+ left_outer_joins!(j)
42
+ end
43
+ end
44
+ @filters << filters
45
+ self
46
+ end
47
+
48
+ def filter_clause_factory
49
+ @filter_clause_factory ||= ActiveRecord::Filter::FilterClauseFactory.new(klass, predicate_builder)
50
+ end
51
+
52
+ def build_arel(aliases = nil)
53
+ arel = super
54
+ my_alias_tracker = ActiveRecord::Associations::AliasTracker.create(connection, table.name, [])
55
+ build_filters(arel, my_alias_tracker)
56
+ arel
57
+ end
58
+
59
+ def build_filters(manager, alias_tracker)
60
+ @filters.each do |filters|
61
+ manager.where(filter_clause_factory.build(filters, alias_tracker).ast)
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,11 @@
1
+ module ActiveRecord::Filter::SpawnMethodsExtension
2
+
3
+ def except(*skips)
4
+ r = relation_with values.except(*skips)
5
+ if !skips.include?(:where)
6
+ r.instance_variable_set(:@filters, instance_variable_get(:@filters))
7
+ end
8
+ r
9
+ end
10
+
11
+ end
@@ -0,0 +1,2 @@
1
+ class ActiveRecord::UnkownFilterError < NoMethodError
2
+ end
@@ -1,5 +1,5 @@
1
1
  module ActiveRecord
2
2
  module Filter
3
- VERSION = '7.0.0'
3
+ VERSION = '7.0.1'
4
4
  end
5
5
  end
@@ -1,25 +1,16 @@
1
1
  require 'active_record'
2
2
  require 'arel/extensions'
3
-
4
- class ActiveRecord::UnkownFilterError < NoMethodError
5
- end
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
3
+ require 'active_record/filter/unkown_filter_error'
20
4
 
21
5
  module ActiveRecord::Filter
22
6
 
7
+ autoload :QueryMethodsExtension, 'active_record/filter/query_methods_extension'
8
+ autoload :AliasTrackerExtension, 'active_record/filter/alias_tracker_extension'
9
+ autoload :FilterClauseFactory, 'active_record/filter/filter_clause_factory'
10
+ autoload :RelationExtension, 'active_record/filter/relation_extension'
11
+ autoload :PredicateBuilderExtension, 'active_record/filter/predicate_builder_extension'
12
+ autoload :SpawnMethodsExtension, 'active_record/filter/spawn_methods_extension'
13
+
23
14
  delegate :filter, :filter_for, to: :all
24
15
 
25
16
  def inherited(subclass)
@@ -32,431 +23,15 @@ module ActiveRecord::Filter
32
23
  end
33
24
 
34
25
  def filter_on(name, dependent_joins=nil, &block)
35
- @filters[name.to_s] = {
36
- joins: dependent_joins,
37
- block: block
38
- }
39
- end
40
-
41
- end
42
-
43
- module ActiveRecord
44
- class PredicateBuilder # :nodoc:
45
-
46
- def self.filter_joins(klass, filters)
47
- custom = []
48
- [build_filter_joins(klass, filters, [], custom), custom]
49
- end
50
-
51
- def self.build_filter_joins(klass, filters, relations=[], custom=[])
52
- if filters.is_a?(Array)
53
- filters.each { |f| build_filter_joins(klass, f, relations, custom) }.compact
54
- elsif filters.is_a?(Hash)
55
- filters.each do |key, value|
56
- if klass.filters.has_key?(key.to_sym)
57
- js = klass.filters.dig(key.to_sym, :joins)
58
-
59
- if js.is_a?(Array)
60
- js.each do |j|
61
- if j.is_a?(String)
62
- custom << j
63
- else
64
- relations << j
65
- end
66
- end
67
- elsif js
68
- if js.is_a?(String)
69
- custom << js
70
- else
71
- relations << js
72
- end
73
- end
74
- elsif reflection = klass._reflections[key.to_s]
75
- if value.is_a?(Hash)
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
103
- elsif value != true && value != false && value != 'true' && value != 'false' && !value.nil?
104
- relations << key
105
- end
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')]
107
- relations << reflection.name
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 }
109
- reflection = klass._reflections[klass._reflections[reflection.name.to_s].send(:delegate_reflection).options[:through].to_s]
110
- relations << {reflection.name => build_filter_joins(reflection.klass, value)}
111
- end
112
- end
113
- end
114
-
115
- relations
116
- end
117
-
118
- def build_from_filter_hash(attributes, relation_trail, alias_tracker)
119
- if attributes.is_a?(Array)
120
- node = build_from_filter_hash(attributes.shift, relation_trail, alias_tracker)
121
-
122
- n = attributes.shift(2)
123
- while !n.empty?
124
- n[1] = build_from_filter_hash(n[1], relation_trail, alias_tracker)
125
- if n[0] == 'AND'
126
- if node.is_a?(Arel::Nodes::And)
127
- node.children.push(n[1])
128
- else
129
- node = node.and(n[1])
130
- end
131
- elsif n[0] == 'OR'
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
140
- else
141
- raise 'lll'
142
- end
143
- n = attributes.shift(2)
144
- end
145
-
146
- node
147
- elsif attributes.is_a?(Hash)
148
- expand_from_filter_hash(attributes, relation_trail, alias_tracker)
149
- else
150
- expand_from_filter_hash({id: attributes}, relation_trail, alias_tracker)
151
- end
152
- end
153
-
154
- def expand_from_filter_hash(attributes, relation_trail, alias_tracker)
155
- klass = table.send(:klass)
156
-
157
- children = attributes.flat_map do |key, value|
158
- if custom_filter = klass.filters[key]
159
- self.instance_exec(klass, table, key, value, relation_trail, alias_tracker, &custom_filter[:block])
160
- elsif column = klass.columns_hash[key.to_s] || klass.columns_hash[key.to_s.split('.').first]
161
- expand_filter_for_column(key, column, value, relation_trail)
162
- elsif relation = klass.reflect_on_association(key)
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)
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 }
167
- expand_filter_for_join_table(relation, value, relation_trail, alias_tracker)
168
- else
169
- raise ActiveRecord::UnkownFilterError.new("Unkown filter \"#{key}\" for #{klass}.")
170
- end
171
- end
172
-
173
- children.compact!
174
- if children.size > 1
175
- Arel::Nodes::And.new(children)
176
- else
177
- children.first
178
- end
179
- end
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)
185
- end
186
-
187
- if column.type == :json || column.type == :jsonb
188
- names = key.to_s.split('.')
189
- names.shift
190
- attribute = attribute.dig(names)
191
- end
192
-
193
- if value.is_a?(Hash)
194
- nodes = value.map do |subkey, subvalue|
195
- expand_filter_for_arel_attribute(column, attribute, subkey, subvalue)
196
- end
197
- nodes.inject { |c, n| c.nil? ? n : c.and(n) }
198
- elsif value == nil
199
- attribute.eq(nil)
200
- elsif value == true || value == 'true'
201
- column.type == :boolean ? attribute.eq(true) : attribute.not_eq(nil)
202
- elsif value == false || value == 'false'
203
- column.type == :boolean ? attribute.eq(false) : attribute.eq(nil)
204
- elsif value.is_a?(Array) && !column.array
205
- attribute.in(value)
206
- elsif column.type != :json && column.type != :jsonb
207
- converted_value = column.array ? Array(value) : value
208
- attribute.eq(converted_value)
209
- else
210
- raise ActiveRecord::UnkownFilterError.new("Unkown type for #{column}. (type #{value.class})")
211
- end
212
-
213
- end
214
-
215
- def expand_filter_for_arel_attribute(column, attribute, key, value)
216
- case key.to_sym
217
- when :contains
218
- attribute.contains(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
219
- when :contained_by
220
- attribute.contained_by(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
221
- when :equal_to, :eq
222
- attribute.eq(value)
223
- when :excludes
224
- attribute.excludes(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
225
- when :greater_than, :gt
226
- attribute.gt(value)
227
- when :greater_than_or_equal_to, :gteq, :gte
228
- attribute.gteq(value)
229
- when :has_key
230
- attribute.has_key(value)
231
- when :has_keys
232
- attribute.has_keys(*Array(value).map { |x| Arel::Nodes.build_quoted(x) })
233
- when :has_any_key
234
- attribute.has_any_key(*Array(value).map { |x| Arel::Nodes.build_quoted(x) })
235
- when :in
236
- attribute.in(value)
237
- when :intersects
238
- # geometry_value = if value.is_a?(Hash) # GeoJSON
239
- # Arel::Nodes::NamedFunction.new('ST_GeomFromGeoJSON', [JSON.generate(value)])
240
- # elsif # EWKB
241
- # elsif # WKB
242
- # elsif # EWKT
243
- # elsif # WKT
244
- # end
245
-
246
- # TODO us above if to determin if SRID sent
247
- geometry_value = if value.is_a?(Hash)
248
- Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromGeoJSON', [Arel::Nodes.build_quoted(JSON.generate(subvalue))]), 4326])
249
- elsif value[0,1] == "\x00" || value[0,1] == "\x01" || value[0,4] =~ /[0-9a-fA-F]{4}/
250
- Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromEWKB', [Arel::Nodes.build_quoted(subvalue)]), 4326])
251
- else
252
- Arel::Nodes::NamedFunction.new('ST_SetSRID', [Arel::Nodes::NamedFunction.new('ST_GeomFromText', [Arel::Nodes.build_quoted(subvalue)]), 4326])
253
- end
254
-
255
- Arel::Nodes::NamedFunction.new('ST_Intersects', [attribute, geometry_value])
256
- when :less_than, :lt
257
- attribute.lt(value)
258
- when :less_than_or_equal_to, :lteq, :lte
259
- attribute.lteq(value)
260
- when :like
261
- attribute.matches(value, nil, true)
262
- when :ilike
263
- attribute.matches(value, nil, false)
264
- when :not, :not_equal, :neq
265
- attribute.not_eq(value)
266
- when :not_in
267
- attribute.not_in(value)
268
- when :overlaps
269
- attribute.overlaps(Arel::Nodes::Casted.new(column.array ? Array(value) : value, attribute))
270
- when :not_overlaps
271
- attribute.not_overlaps(value)
272
- when :ts_match
273
- if value.is_a?(Array)
274
- attribute.ts_query(*value)
275
- else
276
- attribute.ts_query(value)
277
- end
278
- when :within
279
- if value.is_a?(String)
280
- if /\A[0-9A-F]*\Z/i.match?(value) && (value.start_with?('00') || value.start_with?('01'))
281
- attribute.within(Arel::Nodes::HexEncodedBinary.new(value))
282
- else
283
- attribute.within(Arel::Nodes.build_quoted(value))
284
- end
285
- elsif value.is_a?(Hash)
286
- attribute.within(Arel::Nodes.build_quoted(value))
287
- else
288
- raise "Not Supported value for within: #{value.inspect}"
289
- end
290
- else
291
- raise "Not Supported: #{key.to_sym} on column \"#{column.name}\" of type #{column.type}"
292
- end
293
- end
294
-
295
- def expand_filter_for_relationship(relation, value, relation_trail, alias_tracker)
296
- case relation.macro
297
- when :has_many
298
- if value == true || value == 'true'
299
- counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
300
- if relation.active_record.column_names.include?(counter_cache_column_name.to_s)
301
- return table.arel_table[counter_cache_column_name.to_sym].gt(0)
302
- else
303
- raise "Not Supported: #{relation.name}"
304
- end
305
- elsif value == false || value == 'false'
306
- counter_cache_column_name = relation.counter_cache_column || "#{relation.plural_name}_count"
307
- if relation.active_record.column_names.include?(counter_cache_column_name.to_s)
308
- return table.arel_table[counter_cache_column_name.to_sym].eq(0)
309
- else
310
- raise "Not Supported: #{relation.name}"
311
- end
312
- end
313
-
314
- when :belongs_to
315
- if value == true || value == 'true'
316
- return table.arel_table[relation.foreign_key].not_eq(nil)
317
- elsif value == false || value == 'false' || value.nil?
318
- return table.arel_table[relation.foreign_key].eq(nil)
319
- end
320
- end
321
-
322
- if relation.polymorphic?
323
- value = value.dup
324
- klass = value.delete(:as).safe_constantize
325
-
326
- builder = self.class.new(TableMetadata.new(
327
- klass,
328
- alias_tracker.aliased_table_for_relation(relation_trail + ["#{klass.table_name}_as_#{relation.name}"], klass.arel_table) { klass.arel_table.name },
329
- relation
330
- ))
331
- builder.build_from_filter_hash(value, relation_trail + ["#{klass.table_name}_as_#{relation.name}"], alias_tracker)
332
- else
333
- builder = self.class.new(TableMetadata.new(
334
- relation.klass,
335
- 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) },
336
- relation
337
- ))
338
- builder.build_from_filter_hash(value, relation_trail + [relation.name], alias_tracker)
339
- end
340
-
341
- end
342
-
343
-
344
- def expand_filter_for_join_table(relation, value, relation_trail, alias_tracker)
345
- relation = relation.active_record._reflections[relation.active_record._reflections[relation.name.to_s].send(:delegate_reflection).options[:through].to_s]
346
- builder = self.class.new(TableMetadata.new(
347
- relation.klass,
348
- 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) },
349
- relation
350
- ))
351
- builder.build_from_filter_hash(value, relation_trail + [relation.name], alias_tracker)
352
- end
353
-
354
- end
355
- end
356
-
357
-
358
- module ActiveRecord
359
- class Relation
360
- class FilterClauseFactory # :nodoc:
361
- def initialize(klass, predicate_builder)
362
- @klass = klass
363
- @predicate_builder = predicate_builder
364
- end
365
-
366
- def build(filters, alias_tracker)
367
- if filters.is_a?(Hash) || filters.is_a?(Array)
368
- parts = [predicate_builder.build_from_filter_hash(filters, [], alias_tracker)]
369
- else
370
- raise ArgumentError, "Unsupported argument type: #{filters.inspect} (#{filters.class})"
371
- end
372
-
373
- WhereClause.new(parts)
374
- end
375
-
376
- protected
377
-
378
- attr_reader :klass, :predicate_builder
379
- end
26
+ @filters[name.to_s] = { joins: dependent_joins, block: block }
380
27
  end
381
- end
382
-
383
- class ActiveRecord::Relation
384
- module Filter
385
-
386
- def initialize(klass, table: klass.arel_table, predicate_builder: klass.predicate_builder, values: {})
387
- @filters = []
388
- super
389
- end
390
-
391
- def initialize_copy(other)
392
- @filters = @filters.deep_dup
393
- super
394
- end
395
-
396
- def clean_filters(value)
397
- if value.class.name == 'ActionController::Parameters'.freeze
398
- value.to_unsafe_h
399
- elsif value.is_a?(Array)
400
- value.map { |v| clean_filters(v) }
401
- else
402
- value
403
- end
404
- end
405
-
406
- def filter(filters)
407
- filters = clean_filters(filters)
408
28
 
409
- if filters.nil? || filters.empty?
410
- self
411
- else
412
- spawn.filter!(filters)
413
- end
414
- end
415
-
416
- def filter!(filters)
417
- js = ActiveRecord::PredicateBuilder.filter_joins(klass, filters)
418
- js.flatten.each do |j|
419
- if j.is_a?(String)
420
- joins!(j)
421
- elsif j.is_a?(Arel::Nodes::Join)
422
- joins!(j)
423
- elsif j.present?
424
- left_outer_joins!(j)
425
- end
426
- end
427
- @filters << filters
428
- self
429
- end
430
-
431
- def filter_clause_factory
432
- @filter_clause_factory ||= FilterClauseFactory.new(klass, predicate_builder)
433
- end
434
-
435
- def build_arel(aliases = nil)
436
- arel = super
437
- my_alias_tracker = ActiveRecord::Associations::AliasTracker.create(connection, table.name, [])
438
- build_filters(arel, my_alias_tracker)
439
- arel
440
- end
441
-
442
- def build_filters(manager, alias_tracker)
443
- @filters.each do |filters|
444
- manager.where(filter_clause_factory.build(filters, alias_tracker).ast)
445
- end
446
- end
447
-
448
- end
449
29
  end
450
30
 
451
- module ActiveRecord::SpawnMethods
452
- def except(*skips)
453
- r = relation_with values.except(*skips)
454
- if !skips.include?(:where)
455
- r.instance_variable_set(:@filters, instance_variable_get(:@filters))
456
- end
457
- r
458
- end
459
- end
460
31
 
461
- ActiveRecord::Relation.prepend(ActiveRecord::Relation::Filter)
32
+ ActiveRecord::QueryMethods.prepend(ActiveRecord::Filter::QueryMethodsExtension)
462
33
  ActiveRecord::Base.extend(ActiveRecord::Filter)
34
+ ActiveRecord::Relation.prepend(ActiveRecord::Filter::RelationExtension)
35
+ ActiveRecord::SpawnMethods.extend(ActiveRecord::Filter::SpawnMethodsExtension)
36
+ ActiveRecord::PredicateBuilder.include(ActiveRecord::Filter::PredicateBuilderExtension)
37
+ ActiveRecord::Associations::AliasTracker.prepend(ActiveRecord::Filter::AliasTrackerExtension)
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: 7.0.0
4
+ version: 7.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jon Bracy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-12-07 00:00:00.000000000 Z
11
+ date: 2024-09-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -30,14 +30,14 @@ dependencies:
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: 7.0.0
33
+ version: 7.0.3
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: 7.0.0
40
+ version: 7.0.3
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: pg
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -178,6 +178,20 @@ dependencies:
178
178
  - - ">="
179
179
  - !ruby/object:Gem::Version
180
180
  version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: activerecord-postgis-adapter
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
181
195
  description: A safe way to accept user parameters and query against your ActiveRecord
182
196
  Models
183
197
  email:
@@ -189,6 +203,13 @@ extra_rdoc_files:
189
203
  files:
190
204
  - README.md
191
205
  - lib/active_record/filter.rb
206
+ - lib/active_record/filter/alias_tracker_extension.rb
207
+ - lib/active_record/filter/filter_clause_factory.rb
208
+ - lib/active_record/filter/predicate_builder_extension.rb
209
+ - lib/active_record/filter/query_methods_extension.rb
210
+ - lib/active_record/filter/relation_extension.rb
211
+ - lib/active_record/filter/spawn_methods_extension.rb
212
+ - lib/active_record/filter/unkown_filter_error.rb
192
213
  - lib/active_record/filter/version.rb
193
214
  homepage: https://github.com/malomalo/activerecord-filter
194
215
  licenses:
@@ -211,7 +232,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
211
232
  - !ruby/object:Gem::Version
212
233
  version: '0'
213
234
  requirements: []
214
- rubygems_version: 3.2.22
235
+ rubygems_version: 3.5.11
215
236
  signing_key:
216
237
  specification_version: 4
217
238
  summary: A safe way to accept user parameters and query against your ActiveRecord