activerecord-filter 7.0.0 → 7.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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