ransack 5.0.1 → 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 +61 -0
  14. data/lib/ransack/context.rb +170 -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 +35 -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 -375
@@ -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,375 +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
- exists = false
68
- if ransackable_attribute?(str, klass) ||
69
- ransortable_attribute?(str, klass)
70
- exists = true
71
- elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
72
- remainder = []
73
- found_assoc = nil
74
- while !found_assoc && remainder.unshift(segments.pop) &&
75
- segments.size > 0 do
76
- assoc, poly_class = unpolymorphize_association(
77
- segments.join(Constants::UNDERSCORE)
78
- )
79
- if found_assoc = get_association(assoc, klass)
80
- exists = attribute_method?(
81
- remainder.join(Constants::UNDERSCORE),
82
- poly_class || found_assoc.klass
83
- )
84
- end
85
- end
86
- end
87
- exists
88
- end
89
-
90
- def table_for(parent)
91
- parent.table
92
- end
93
-
94
- def klassify(obj)
95
- if Class === obj && ::ActiveRecord::Base > obj
96
- obj
97
- elsif obj.respond_to? :klass
98
- obj.klass
99
- elsif obj.respond_to? :base_klass
100
- obj.base_klass
101
- else
102
- raise ArgumentError, "Don't know how to klassify #{obj}"
103
- end
104
- end
105
-
106
- # All dependent Arel::Join nodes used in the search query.
107
- #
108
- # This could otherwise be done as `@object.arel.join_sources`, except
109
- # that ActiveRecord's build_joins sets up its own JoinDependency.
110
- # This extracts what we need to access the joins using our existing
111
- # JoinDependency to track table aliases.
112
- #
113
- def join_sources
114
- base, joins = begin
115
- alias_tracker = @object.alias_tracker
116
- constraints = @join_dependency.join_constraints(@object.joins_values, alias_tracker, @object.references_values)
117
-
118
- [
119
- Arel::SelectManager.new(@object.table),
120
- constraints
121
- ]
122
- end
123
- joins.each do |aliased_join|
124
- base.from(aliased_join)
125
- end
126
- base.join_sources
127
- end
128
-
129
- def alias_tracker
130
- @join_dependency.send(:alias_tracker)
131
- end
132
-
133
- def lock_association(association)
134
- @lock_associations << association
135
- end
136
-
137
- def remove_association(association)
138
- return if @lock_associations.include?(association)
139
- @join_dependency.instance_variable_get(:@join_root).children.delete_if { |stashed|
140
- stashed.eql?(association)
141
- }
142
- @object.joins_values.delete_if { |jd|
143
- jd.instance_variables.include?(:@join_root) &&
144
- jd.instance_variable_get(:@join_root).children.map(&:object_id) == [association.object_id]
145
- }
146
- end
147
-
148
- # Build an Arel subquery that selects keys for the top query,
149
- # drawn from the first join association's foreign_key.
150
- #
151
- # Example: for an Article that has_and_belongs_to_many Tags
152
- #
153
- # context = Article.search.context
154
- # attribute = Attribute.new(context, "tags_name").tap do |a|
155
- # context.bind(a, a.name)
156
- # end
157
- # context.build_correlated_subquery(attribute.parent).to_sql
158
- #
159
- # # SELECT "articles_tags"."article_id" FROM "articles_tags"
160
- # # INNER JOIN "tags" ON "tags"."id" = "articles_tags"."tag_id"
161
- # # WHERE "articles_tags"."article_id" = "articles"."id"
162
- #
163
- # The WHERE condition on this query makes it invalid by itself,
164
- # because it is correlated to the primary key on the outer query.
165
- #
166
- def build_correlated_subquery(association)
167
- join_constraints = extract_joins(association)
168
- join_root = join_constraints.shift
169
- correlated_key = extract_correlated_key(join_root)
170
- subquery = Arel::SelectManager.new(association.base_klass)
171
- subquery.from(join_root.left)
172
- subquery.project(correlated_key)
173
- join_constraints.each do |j|
174
- subquery.join_sources << Arel::Nodes::InnerJoin.new(j.left, j.right)
175
- end
176
-
177
- # Handle polymorphic associations where correlated_key is an array
178
- if correlated_key.is_a?(Array)
179
- # For polymorphic associations, we need to add conditions for both the foreign key and type
180
- correlated_key.each_with_index do |key, index|
181
- if index == 0
182
- # This is the foreign key
183
- subquery = subquery.where(key.eq(primary_key))
184
- else
185
- # This is the type key, which should be equal to the model name
186
- subquery = subquery.where(key.eq(@klass.name))
187
- end
188
- end
189
- else
190
- # Original behavior for non-polymorphic associations
191
- subquery = subquery.where(correlated_key.eq(primary_key))
192
- end
193
-
194
- subquery
195
- end
196
-
197
- def primary_key
198
- @object.table[@object.primary_key]
199
- end
200
-
201
- private
202
-
203
- def extract_correlated_key(join_root)
204
- case join_root
205
- when Arel::Nodes::OuterJoin
206
- # one of join_root.right/join_root.left is expected to be Arel::Nodes::On
207
- if join_root.right.is_a?(Arel::Nodes::On)
208
- extract_correlated_key(join_root.right.expr)
209
- elsif join_root.left.is_a?(Arel::Nodes::On)
210
- extract_correlated_key(join_root.left.expr)
211
- else
212
- raise 'Ransack encountered an unexpected arel structure'
213
- end
214
- when Arel::Nodes::Equality
215
- pk = primary_key
216
- if join_root.left.eql?(pk)
217
- join_root.right
218
- elsif join_root.right.eql?(pk)
219
- join_root.left
220
- else
221
- nil
222
- end
223
- when Arel::Nodes::And
224
- # And may have multiple children, so we need to check all, not via left/right
225
- if join_root.children.any?
226
- join_root.children.each do |child|
227
- key = extract_correlated_key(child)
228
- return key if key
229
- end
230
- else
231
- extract_correlated_key(join_root.left) || extract_correlated_key(join_root.right)
232
- end
233
- else
234
- # eg parent was Arel::Nodes::And and the evaluated side was one of
235
- # Arel::Nodes::Grouping or MultiTenant::TenantEnforcementClause
236
- nil
237
- end
238
- end
239
-
240
- def get_parent_and_attribute_name(str, parent = @base)
241
- attr_name = nil
242
-
243
- if ransackable_attribute?(str, klassify(parent)) ||
244
- ransortable_attribute?(str, klassify(parent))
245
- attr_name = str
246
- elsif (segments = str.split(Constants::UNDERSCORE)).size > 1
247
- remainder = []
248
- found_assoc = nil
249
- while remainder.unshift(segments.pop) && segments.size > 0 &&
250
- !found_assoc do
251
- assoc, klass = unpolymorphize_association(
252
- segments.join(Constants::UNDERSCORE)
253
- )
254
- if found_assoc = get_association(assoc, parent)
255
- join = build_or_find_association(
256
- found_assoc.name, parent, klass
257
- )
258
- parent, attr_name = get_parent_and_attribute_name(
259
- remainder.join(Constants::UNDERSCORE), join
260
- )
261
- end
262
- end
263
- end
264
-
265
- [parent, attr_name]
266
- end
267
-
268
- def get_association(str, parent = @base)
269
- klass = klassify parent
270
- ransackable_association?(str, klass) &&
271
- klass.reflect_on_all_associations.detect { |a| a.name.to_s == str }
272
- end
273
-
274
- def join_dependency(relation)
275
- if relation.respond_to?(:join_dependency) # Polyamorous enables this
276
- relation.join_dependency
277
- else
278
- build_joins(relation)
279
- end
280
- end
281
-
282
- # Checkout active_record/relation/query_methods.rb +build_joins+ for
283
- # reference. Lots of duplicated code maybe we can avoid it
284
- def build_joins(relation)
285
- buckets = relation.joins_values + relation.left_outer_joins_values
286
-
287
- buckets = buckets.group_by do |join|
288
- case join
289
- when String
290
- :string_join
291
- when Hash, Symbol, Array
292
- :association_join
293
- when Polyamorous::JoinDependency, Polyamorous::JoinAssociation
294
- :stashed_join
295
- when Arel::Nodes::Join
296
- :join_node
297
- else
298
- raise 'unknown class: %s' % join.class.name
299
- end
300
- end
301
- buckets.default = []
302
- association_joins = buckets[:association_join]
303
- stashed_association_joins = buckets[:stashed_join]
304
- join_nodes = buckets[:join_node].uniq
305
- string_joins = buckets[:string_join].map(&:strip)
306
- string_joins.uniq!
307
-
308
- join_list = join_nodes + convert_join_strings_to_ast(relation.table, string_joins)
309
-
310
- alias_tracker = relation.alias_tracker(join_list)
311
- join_dependency = Polyamorous::JoinDependency.new(relation.klass, relation.table, association_joins, Arel::Nodes::OuterJoin)
312
- join_dependency.instance_variable_set(:@alias_tracker, alias_tracker)
313
- join_nodes.each do |join|
314
- join_dependency.send(:alias_tracker).aliases[join.left.name.downcase] = 1
315
- end
316
- join_dependency
317
- end
318
-
319
- def convert_join_strings_to_ast(table, joins)
320
- joins.map! { |join| table.create_string_join(Arel.sql(join)) unless join.blank? }
321
- joins.compact!
322
- joins
323
- end
324
-
325
- def build_or_find_association(name, parent = @base, klass = nil)
326
- find_association(name, parent, klass) or build_association(name, parent, klass)
327
- end
328
-
329
- def find_association(name, parent = @base, klass = nil)
330
- @join_dependency.instance_variable_get(:@join_root).children.detect do |assoc|
331
- assoc.reflection.name == name && assoc.table &&
332
- (@associations_pot.empty? || @associations_pot[assoc] == parent || !@associations_pot.key?(assoc)) &&
333
- (!klass || assoc.reflection.klass == klass)
334
- end
335
- end
336
-
337
- def build_association(name, parent = @base, klass = nil)
338
- jd = Polyamorous::JoinDependency.new(
339
- parent.base_klass,
340
- parent.table,
341
- Polyamorous::Join.new(name, @join_type, klass),
342
- @join_type
343
- )
344
- found_association = jd.instance_variable_get(:@join_root).children.last
345
-
346
- @associations_pot[found_association] = parent
347
-
348
- # TODO maybe we dont need to push associations here, we could loop
349
- # through the @associations_pot instead
350
- @join_dependency.instance_variable_get(:@join_root).children.push found_association
351
-
352
- # Builds the arel nodes properly for this association
353
- @tables_pot[found_association] = @join_dependency.construct_tables_for_association!(jd.instance_variable_get(:@join_root), found_association)
354
-
355
- # Leverage the stashed association functionality in AR
356
- @object = @object.joins(jd)
357
- found_association
358
- end
359
-
360
- def extract_joins(association)
361
- parent = @join_dependency.instance_variable_get(:@join_root)
362
- reflection = association.reflection
363
- join_constraints = association.join_constraints_with_tables(
364
- parent.table,
365
- parent.base_klass,
366
- Arel::Nodes::OuterJoin,
367
- @join_dependency.instance_variable_get(:@alias_tracker),
368
- @tables_pot[association]
369
- )
370
- join_constraints.to_a.flatten
371
- end
372
- end
373
- end
374
- end
375
- end