ransack 5.0.2 → 6.0.0

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.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +13 -27
  3. data/lib/ransack/active_record/base.rb +154 -0
  4. data/lib/ransack/active_record/context.rb +504 -0
  5. data/lib/ransack/active_record/dialect.rb +83 -0
  6. data/lib/ransack/active_record/join.rb +75 -0
  7. data/lib/ransack/active_record/join_association.rb +104 -0
  8. data/lib/ransack/active_record/join_dependency.rb +123 -0
  9. data/lib/ransack/active_record/reflection.rb +18 -0
  10. data/lib/ransack/active_record/tree_node.rb +13 -0
  11. data/lib/ransack/active_record.rb +44 -9
  12. data/lib/ransack/configuration.rb +49 -1
  13. data/lib/ransack/constants.rb +52 -0
  14. data/lib/ransack/context.rb +159 -35
  15. data/lib/ransack/helpers/form_builder.rb +30 -25
  16. data/lib/ransack/nodes/bindable.rb +0 -1
  17. data/lib/ransack/nodes/condition.rb +215 -73
  18. data/lib/ransack/nodes/grouping.rb +33 -11
  19. data/lib/ransack/nodes/sort.rb +1 -1
  20. data/lib/ransack/nodes/value.rb +8 -1
  21. data/lib/ransack/predicate.rb +8 -1
  22. data/lib/ransack/search.rb +52 -9
  23. data/lib/ransack/version.rb +1 -1
  24. data/lib/ransack.rb +4 -1
  25. metadata +11 -13
  26. data/lib/polyamorous/activerecord/join_association.rb +0 -70
  27. data/lib/polyamorous/activerecord/join_association_7_2.rb +0 -55
  28. data/lib/polyamorous/activerecord/join_dependency.rb +0 -102
  29. data/lib/polyamorous/activerecord/reflection.rb +0 -11
  30. data/lib/polyamorous/join.rb +0 -70
  31. data/lib/polyamorous/polyamorous.rb +0 -27
  32. data/lib/polyamorous/swapping_reflection_class.rb +0 -11
  33. data/lib/polyamorous/tree_node.rb +0 -7
  34. data/lib/ransack/adapters/active_record/base.rb +0 -152
  35. data/lib/ransack/adapters/active_record/context.rb +0 -379
@@ -1,152 +0,0 @@
1
- module Ransack
2
- module Adapters
3
- module ActiveRecord
4
- module Base
5
-
6
- def self.extended(base)
7
- base.class_eval do
8
- class_attribute :_ransackers
9
- class_attribute :_ransack_aliases
10
- self._ransackers ||= {}
11
- self._ransack_aliases ||= {}
12
- end
13
- end
14
-
15
- def ransack(params = {}, options = {})
16
- Search.new(self, params, options)
17
- end
18
-
19
- def ransack!(params = {}, options = {})
20
- ransack(params, options.merge(ignore_unknown_conditions: false))
21
- end
22
-
23
- def ransacker(name, opts = {}, &block)
24
- self._ransackers = _ransackers.merge name.to_s => Ransacker
25
- .new(self, name, opts, &block)
26
- end
27
-
28
- def ransack_alias(new_name, old_name)
29
- self._ransack_aliases = _ransack_aliases.merge new_name.to_s =>
30
- old_name.to_s
31
- end
32
-
33
- # Ransackable_attributes, by default, returns all column names
34
- # and any defined ransackers as an array of strings.
35
- # For overriding with a whitelist array of strings.
36
- #
37
- def ransackable_attributes(auth_object = nil)
38
- @ransackable_attributes ||= deprecated_ransackable_list(:ransackable_attributes)
39
- end
40
-
41
- # Ransackable_associations, by default, returns the names
42
- # of all associations as an array of strings.
43
- # For overriding with a whitelist array of strings.
44
- #
45
- def ransackable_associations(auth_object = nil)
46
- @ransackable_associations ||= deprecated_ransackable_list(:ransackable_associations)
47
- end
48
-
49
- # Ransortable_attributes, by default, returns the names
50
- # of all attributes available for sorting as an array of strings.
51
- # For overriding with a whitelist array of strings.
52
- #
53
- def ransortable_attributes(auth_object = nil)
54
- ransackable_attributes(auth_object)
55
- end
56
-
57
- # Ransackable_scopes, by default, returns an empty array
58
- # i.e. no class methods/scopes are authorized.
59
- # For overriding with a whitelist array of *symbols*.
60
- #
61
- def ransackable_scopes(auth_object = nil)
62
- []
63
- end
64
-
65
- # ransack_scope_skip_sanitize_args, by default, returns an empty array.
66
- # i.e. use the sanitize_scope_args setting to determine if args should be converted.
67
- # For overriding with a list of scopes which should be passed the args as-is.
68
- #
69
- def ransackable_scopes_skip_sanitize_args
70
- []
71
- end
72
-
73
- # Bare list of all potentially searchable attributes. Searchable attributes
74
- # need to be explicitly allowlisted through the `ransackable_attributes`
75
- # method in each model, but if you're allowing almost everything to be
76
- # searched, this list can be used as a base for exclusions.
77
- #
78
- def authorizable_ransackable_attributes
79
- if Ransack::SUPPORTS_ATTRIBUTE_ALIAS
80
- column_names + _ransackers.keys + _ransack_aliases.keys +
81
- attribute_aliases.keys
82
- else
83
- column_names + _ransackers.keys + _ransack_aliases.keys
84
- end.uniq
85
- end
86
-
87
- # Bare list of all potentially searchable associations. Searchable
88
- # associations need to be explicitly allowlisted through the
89
- # `ransackable_associations` method in each model, but if you're
90
- # allowing almost everything to be searched, this list can be used as a
91
- # base for exclusions.
92
- #
93
- def authorizable_ransackable_associations
94
- reflect_on_all_associations.map { |a| a.name.to_s }
95
- end
96
-
97
- private
98
-
99
- def deprecated_ransackable_list(method)
100
- list_type = method.to_s.delete_prefix("ransackable_")
101
-
102
- if explicitly_defined?(method)
103
- warn_deprecated <<~ERROR
104
- Ransack's builtin `#{method}` method is deprecated and will result
105
- in an error in the future. If you want to authorize the full list
106
- of searchable #{list_type} for this model, use
107
- `authorizable_#{method}` instead of delegating to `super`.
108
- ERROR
109
-
110
- public_send("authorizable_#{method}")
111
- else
112
- raise <<~MESSAGE
113
- Ransack needs #{name} #{list_type} explicitly allowlisted as
114
- searchable. Define a `#{method}` class method in your `#{name}`
115
- model, watching out for items you DON'T want searchable (for
116
- example, `encrypted_password`, `password_reset_token`, `owner` or
117
- other sensitive information). You can use the following as a base:
118
-
119
- ```ruby
120
- class #{name} < ApplicationRecord
121
-
122
- # ...
123
-
124
- def self.#{method}(auth_object = nil)
125
- #{public_send("authorizable_#{method}").sort.inspect}
126
- end
127
-
128
- # ...
129
-
130
- end
131
- ```
132
- MESSAGE
133
- end
134
- end
135
-
136
- def explicitly_defined?(method)
137
- definer_ancestor = singleton_class.ancestors.find do |ancestor|
138
- ancestor.instance_methods(false).include?(method)
139
- end
140
-
141
- definer_ancestor != Ransack::Adapters::ActiveRecord::Base
142
- end
143
-
144
- def warn_deprecated(message)
145
- caller_location = caller_locations.find { |location| !location.path.start_with?(File.expand_path("../..", __dir__)) }
146
-
147
- warn "DEPRECATION WARNING: #{message.squish} (called at #{caller_location.path}:#{caller_location.lineno})"
148
- end
149
- end
150
- end
151
- end
152
- end
@@ -1,379 +0,0 @@
1
- require 'ransack/context'
2
- require 'polyamorous/polyamorous'
3
-
4
- module Ransack
5
- module Adapters
6
- module ActiveRecord
7
- class Context < ::Ransack::Context
8
-
9
- def relation_for(object)
10
- object.all
11
- end
12
-
13
- def type_for(attr)
14
- return nil unless attr && attr.valid?
15
- relation = attr.arel_attribute.relation
16
- name = attr.arel_attribute.name.to_s
17
- table = relation.respond_to?(:table_name) ? relation.table_name : relation.name
18
- schema_cache = self.klass.connection_pool.schema_cache
19
- unless schema_cache.send(:data_source_exists?, table)
20
- raise "No table named #{table} exists."
21
- end
22
- column = attr.klass.columns.find { |column| column.name == name }
23
- column&.type
24
- end
25
-
26
- def evaluate(search, opts = {})
27
- viz = Visitor.new
28
- relation = @object.where(viz.accept(search.base))
29
-
30
- if search.sorts.any?
31
- relation = relation.except(:order)
32
- # Rather than applying all of the search's sorts in one fell swoop,
33
- # as the original implementation does, we apply one at a time.
34
- #
35
- # If the sort (returned by the Visitor above) is a symbol, we know
36
- # that it represents a scope on the model and we can apply that
37
- # scope.
38
- #
39
- # Otherwise, we fall back to the applying the sort with the "order"
40
- # method as the original implementation did. Actually the original
41
- # implementation used "reorder," which was overkill since we already
42
- # have a clean slate after "relation.except(:order)" above.
43
- viz.accept(search.sorts).each do |scope_or_sort|
44
- if scope_or_sort.is_a?(Symbol)
45
- relation = relation.send(scope_or_sort)
46
- else
47
- case Ransack.options[:fields_sort_option]
48
- when :nulls_first
49
- scope_or_sort = scope_or_sort.direction == :asc ? scope_or_sort.nulls_first : scope_or_sort.nulls_last
50
- when :nulls_last
51
- scope_or_sort = scope_or_sort.direction == :asc ? scope_or_sort.nulls_last : scope_or_sort.nulls_first
52
- when :nulls_always_first
53
- scope_or_sort = scope_or_sort.nulls_first
54
- when :nulls_always_last
55
- scope_or_sort = scope_or_sort.nulls_last
56
- end
57
-
58
- relation = relation.order(scope_or_sort)
59
- end
60
- end
61
- end
62
-
63
- opts[:distinct] ? relation.distinct : relation
64
- end
65
-
66
- def attribute_method?(str, klass = @klass)
67
- return false unless key_within_depth_limit?(str)
68
-
69
- exists = false
70
- if ransackable_attribute?(str, klass) ||
71
- ransortable_attribute?(str, klass)
72
- exists = true
73
- elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
74
- remainder = []
75
- found_assoc = nil
76
- while !found_assoc && remainder.unshift(segments.pop) &&
77
- segments.size > 0 do
78
- assoc, poly_class = unpolymorphize_association(
79
- segments.join(Constants::UNDERSCORE)
80
- )
81
- if found_assoc = get_association(assoc, klass)
82
- exists = attribute_method?(
83
- remainder.join(Constants::UNDERSCORE),
84
- poly_class || found_assoc.klass
85
- )
86
- end
87
- end
88
- end
89
- exists
90
- end
91
-
92
- def table_for(parent)
93
- parent.table
94
- end
95
-
96
- def klassify(obj)
97
- if Class === obj && ::ActiveRecord::Base > obj
98
- obj
99
- elsif obj.respond_to? :klass
100
- obj.klass
101
- elsif obj.respond_to? :base_klass
102
- obj.base_klass
103
- else
104
- raise ArgumentError, "Don't know how to klassify #{obj}"
105
- end
106
- end
107
-
108
- # All dependent Arel::Join nodes used in the search query.
109
- #
110
- # This could otherwise be done as `@object.arel.join_sources`, except
111
- # that ActiveRecord's build_joins sets up its own JoinDependency.
112
- # This extracts what we need to access the joins using our existing
113
- # JoinDependency to track table aliases.
114
- #
115
- def join_sources
116
- base, joins = begin
117
- alias_tracker = @object.alias_tracker
118
- constraints = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
119
-
120
- [
121
- Arel::SelectManager.new(@object.table),
122
- constraints
123
- ]
124
- end
125
- joins.each do |aliased_join|
126
- base.from(aliased_join)
127
- end
128
- base.join_sources
129
- end
130
-
131
- def alias_tracker
132
- @join_dependency.send(:alias_tracker)
133
- end
134
-
135
- def lock_association(association)
136
- @lock_associations << association
137
- end
138
-
139
- def remove_association(association)
140
- return if @lock_associations.include?(association)
141
- @join_dependency.instance_variable_get(:@join_root).children.delete_if { |stashed|
142
- stashed.eql?(association)
143
- }
144
- @object.joins_values.delete_if { |jd|
145
- jd.instance_variables.include?(:@join_root) &&
146
- jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
147
- }
148
- end
149
-
150
- # Build an Arel subquery that selects keys for the top query,
151
- # drawn from the first join association's foreign_key.
152
- #
153
- # Example: for an Article that has_and_belongs_to_many Tags
154
- #
155
- # context = Article.search.context
156
- # attribute = Attribute.new(context, "tags_name").tap do |a|
157
- # context.bind(a, a.name)
158
- # end
159
- # context.build_correlated_subquery(attribute.parent).to_sql
160
- #
161
- # # SELECT "articles_tags"."article_id" FROM "articles_tags"
162
- # # INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
163
- # # WHERE "articles_tags"."article_id" = "articles"."id"
164
- #
165
- # The WHERE condition on this query makes it invalid by itself,
166
- # because it is correlated to the primary key on the outer query.
167
- #
168
- def build_correlated_subquery(association)
169
- join_constraints = extract_joins(association)
170
- join_root = join_constraints.shift
171
- correlated_key = extract_correlated_key(join_root)
172
- subquery = Arel::SelectManager.new(association.base_klass)
173
- subquery.from(join_root.left)
174
- subquery.project(correlated_key)
175
- join_constraints.each do |j|
176
- subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
177
- end
178
-
179
- # Handle polymorphic associations where correlated_key is an array
180
- if correlated_key.is_a?(Array)
181
- # For polymorphic associations, we need to add conditions for both the foreign key and type
182
- correlated_key.each_with_index do |key, index|
183
- if index == 0
184
- # This is the foreign key
185
- subquery = subquery.where(key.eq(primary_key))
186
- else
187
- # This is the type key, which should be equal to the model name
188
- subquery = subquery.where(key.eq(@klass.name))
189
- end
190
- end
191
- else
192
- # Original behavior for non-polymorphic associations
193
- subquery = subquery.where(correlated_key.eq(primary_key))
194
- end
195
-
196
- subquery
197
- end
198
-
199
- def primary_key
200
- @object.table[@object.primary_key]
201
- end
202
-
203
- private
204
-
205
- def extract_correlated_key(join_root)
206
- case join_root
207
- when Arel::Nodes::OuterJoin
208
- # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
209
- if join_root.right.is_a?(Arel::Nodes::On)
210
- extract_correlated_key(join_root.right.expr)
211
- elsif join_root.left.is_a?(Arel::Nodes::On)
212
- extract_correlated_key(join_root.left.expr)
213
- else
214
- raise 'Ransack encountered an unexpected arel structure'
215
- end
216
- when Arel::Nodes::Equality
217
- pk = primary_key
218
- if join_root.left.eql?(pk)
219
- join_root.right
220
- elsif join_root.right.eql?(pk)
221
- join_root.left
222
- else
223
- nil
224
- end
225
- when Arel::Nodes::And
226
- # And may have multiple children, so we need to check all, not via left/right
227
- if join_root.children.any?
228
- join_root.children.each do |child|
229
- key = extract_correlated_key(child)
230
- return key if key
231
- end
232
- else
233
- extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
234
- end
235
- else
236
- # eg parent was Arel::Nodes::And and the evaluated side was one of
237
- # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
238
- nil
239
- end
240
- end
241
-
242
- def get_parent_and_attribute_name(str, parent = @base)
243
- return [parent, nil] unless key_within_depth_limit?(str)
244
-
245
- attr_name = nil
246
-
247
- if ransackable_attribute?(str, klassify(parent)) ||
248
- ransortable_attribute?(str, klassify(parent))
249
- attr_name = str
250
- elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
251
- remainder = []
252
- found_assoc = nil
253
- while remainder.unshift(segments.pop) && segments.size > 0 &&
254
- !found_assoc do
255
- assoc, klass = unpolymorphize_association(
256
- segments.join(Constants::UNDERSCORE)
257
- )
258
- if found_assoc = get_association(assoc, parent)
259
- join = build_or_find_association(
260
- found_assoc.name, parent, klass
261
- )
262
- parent, attr_name = get_parent_and_attribute_name(
263
- remainder.join(Constants::UNDERSCORE), join
264
- )
265
- end
266
- end
267
- end
268
-
269
- [parent, attr_name]
270
- end
271
-
272
- def get_association(str, parent = @base)
273
- klass = klassify parent
274
- ransackable_association?(str, klass) &&
275
- klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
276
- end
277
-
278
- def join_dependency(relation)
279
- if relation.respond_to?(:join_dependency) # Polyamorous enables this
280
- relation.join_dependency
281
- else
282
- build_joins(relation)
283
- end
284
- end
285
-
286
- # Checkout active_record/relation/query_methods.rb +build_joins+ for
287
- # reference. Lots of duplicated code maybe we can avoid it
288
- def build_joins(relation)
289
- buckets = relation.joins_values + relation.left_outer_joins_values
290
-
291
- buckets = buckets.group_by do |join|
292
- case join
293
- when String
294
- :string_join
295
- when Hash, Symbol, Array
296
- :association_join
297
- when Polyamorous::JoinDependency, Polyamorous::JoinAssociation
298
- :stashed_join
299
- when Arel::Nodes::Join
300
- :join_node
301
- else
302
- raise 'unknown class: %s' % join.class.name
303
- end
304
- end
305
- buckets.default = []
306
- association_joins = buckets[:association_join]
307
- stashed_association_joins = buckets[:stashed_join]
308
- join_nodes = buckets[:join_node].uniq
309
- string_joins = buckets[:string_join].map(&:strip)
310
- string_joins.uniq!
311
-
312
- join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
313
-
314
- alias_tracker = relation.alias_tracker(join_list)
315
- join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
316
- join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
317
- join_nodes.each do |join|
318
- join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
319
- end
320
- join_dependency
321
- end
322
-
323
- def convert_join_strings_to_ast(table, joins)
324
- joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
325
- joins.compact!
326
- joins
327
- end
328
-
329
- def build_or_find_association(name, parent = @base, klass = nil)
330
- find_association(name, parent, klass) or build_association(name, parent, klass)
331
- end
332
-
333
- def find_association(name, parent = @base, klass = nil)
334
- @join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
335
- assoc.reflection.name == name && assoc.table &&
336
- (@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
337
- (!klass || assoc.reflection.klass == klass)
338
- end
339
- end
340
-
341
- def build_association(name, parent = @base, klass = nil)
342
- jd = Polyamorous::JoinDependency.new(
343
- parent.base_klass,
344
- parent.table,
345
- Polyamorous::Join.new(name, @join_type, klass),
346
- @join_type
347
- )
348
- found_association = jd.instance_variable_get(:@join_root).children.last
349
-
350
- @associations_pot[found_association] = parent
351
-
352
- # TODO maybe we dont need to push associations here, we could loop
353
- # through the @associations_pot instead
354
- @join_dependency.instance_variable_get(:@join_root).children.push found_association
355
-
356
- # Builds the arel nodes properly for this association
357
- @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
358
-
359
- # Leverage the stashed association functionality in AR
360
- @object = @object.joins(jd)
361
- found_association
362
- end
363
-
364
- def extract_joins(association)
365
- parent = @join_dependency.instance_variable_get(:@join_root)
366
- reflection = association.reflection
367
- join_constraints = association.join_constraints_with_tables(
368
- parent.table,
369
- parent.base_klass,
370
- Arel::Nodes::OuterJoin,
371
- @join_dependency.instance_variable_get(:@alias_tracker),
372
- @tables_pot[association]
373
- )
374
- join_constraints.to_a.flatten
375
- end
376
- end
377
- end
378
- end
379
- end