plutonium 0.10.0 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/app/assets/javascripts/turbo/index.js +1 -1
  4. data/app/views/application/_resource_header.html.erb +12 -12
  5. data/app/views/layouts/resource.html.erb +1 -0
  6. data/app/views/layouts/rodauth.html.erb +4 -4
  7. data/app/views/resource/_nav_user.html.erb +1 -1
  8. data/app/views/resource/index.rabl +1 -1
  9. data/brakeman.ignore +1 -1
  10. data/config/initializers/rabl.rb +2 -0
  11. data/css.manifest +1 -1
  12. data/js.manifest +2 -2
  13. data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +0 -3
  14. data/lib/generators/pu/gen/pug/pug_generator.rb +6 -0
  15. data/lib/generators/pu/gen/pug/templates/pug.rb.tt +1 -1
  16. data/lib/generators/pu/pkg/app/templates/config/routes.rb.tt +3 -3
  17. data/lib/plutonium/config.rb +1 -11
  18. data/lib/plutonium/core/autodiscovery/input_discoverer.rb +1 -1
  19. data/lib/plutonium/core/autodiscovery/renderer_discoverer.rb +1 -1
  20. data/lib/plutonium/core/controllers/base.rb +13 -3
  21. data/lib/plutonium/core/controllers/queryable.rb +3 -1
  22. data/lib/plutonium/pkg/app.rb +6 -0
  23. data/lib/plutonium/railtie.rb +15 -0
  24. data/lib/plutonium/reloader.rb +99 -0
  25. data/lib/plutonium/resource/controller.rb +61 -22
  26. data/lib/plutonium/resource/policy.rb +56 -9
  27. data/lib/plutonium/resource/presenter.rb +44 -12
  28. data/lib/plutonium/resource/query_object.rb +186 -73
  29. data/lib/plutonium/resource/record.rb +213 -119
  30. data/lib/plutonium/rodauth/controller_methods.rb +7 -0
  31. data/lib/plutonium/version.rb +1 -1
  32. data/lib/plutonium.rb +50 -12
  33. data/package-lock.json +174 -0
  34. data/package.json +2 -0
  35. data/public/plutonium-assets/plutonium-app-6WILQCTT.js +39 -0
  36. data/public/plutonium-assets/plutonium-app-6WILQCTT.js.map +7 -0
  37. data/public/plutonium-assets/plutonium-logo-original.png +0 -0
  38. data/public/plutonium-assets/plutonium-logo-white.png +0 -0
  39. data/public/plutonium-assets/plutonium-logo.png +0 -0
  40. data/public/plutonium-assets/plutonium.2d4f0c333cd000051d3b.css +3424 -0
  41. data/public/plutonium-assets/plutonium.ico +0 -0
  42. metadata +10 -4
  43. data/lib/plutonium/reactor/core.rb +0 -78
  44. data/public/plutonium-assets/logo.png +0 -0
@@ -1,108 +1,155 @@
1
1
  module Plutonium
2
2
  module Resource
3
+ # Policy class to define permissions and attributes for a resource
3
4
  class Policy
4
5
  include Plutonium::Policy::Initializer
5
6
 
6
7
  class Scope < Plutonium::Policy::Scope
7
8
  end
8
9
 
10
+ # Sends a method and raises an error if the method is not implemented
11
+ # @param [Symbol] method The method to send
9
12
  def send_with_report(method)
10
- raise NotImplementedError, "#{self.class.name} does not implement the required #{method}" unless respond_to? method
13
+ unless respond_to?(method)
14
+ raise NotImplementedError, "#{self.class.name} does not implement the required #{method}"
15
+ end
11
16
 
12
- send method
17
+ send(method)
13
18
  end
14
19
 
15
20
  # Core actions
16
21
 
22
+ # Checks if the create action is permitted
23
+ # @return [Boolean]
17
24
  def create?
18
25
  false
19
26
  end
20
27
 
28
+ # Checks if the read action is permitted
29
+ # @return [Boolean]
21
30
  def read?
22
31
  false
23
32
  end
24
33
 
34
+ # Checks if the update action is permitted
35
+ # @return [Boolean]
25
36
  def update?
26
37
  create?
27
38
  end
28
39
 
40
+ # Checks if the destroy action is permitted
41
+ # @return [Boolean]
29
42
  def destroy?
30
43
  create?
31
44
  end
32
45
 
33
46
  # Inferred actions
34
47
 
48
+ # Checks if the index action is permitted
49
+ # @return [Boolean]
35
50
  def index?
36
51
  read?
37
52
  end
38
53
 
54
+ # Checks if the new action is permitted
55
+ # @return [Boolean]
39
56
  def new?
40
57
  create?
41
58
  end
42
59
 
60
+ # Checks if the show action is permitted
61
+ # @return [Boolean]
43
62
  def show?
44
63
  read?
45
64
  end
46
65
 
66
+ # Checks if the edit action is permitted
67
+ # @return [Boolean]
47
68
  def edit?
48
69
  update?
49
70
  end
50
71
 
72
+ # Checks if the search action is permitted
73
+ # @return [Boolean]
51
74
  def search?
52
75
  index?
53
76
  end
54
77
 
55
78
  # Core attributes
56
79
 
80
+ # Returns the permitted attributes for the create action
81
+ # @return [Array<Symbol>]
57
82
  def permitted_attributes_for_create
58
- autodetect_fields_for(:permitted_attributes_for_create) - [
83
+ autodetect_permitted_fields(:permitted_attributes_for_create) - [
59
84
  context.resource_context.resource_class.primary_key.to_sym, # primary_key
60
85
  :created_at, :updated_at # timestamps
61
86
  ]
62
87
  end
63
88
 
89
+ # Returns the permitted attributes for the read action
90
+ # @return [Array<Symbol>]
64
91
  def permitted_attributes_for_read
65
- autodetect_fields_for :permitted_attributes_for_read
92
+ autodetect_permitted_fields(:permitted_attributes_for_read)
66
93
  end
67
94
 
95
+ # Returns the permitted attributes for the update action
96
+ # @return [Array<Symbol>]
68
97
  def permitted_attributes_for_update
69
98
  permitted_attributes_for_create
70
99
  end
71
100
 
72
101
  # Inferred attributes
73
102
 
103
+ # Returns the permitted attributes for the index action
104
+ # @return [Array<Symbol>]
74
105
  def permitted_attributes_for_index
75
106
  permitted_attributes_for_read
76
107
  end
77
108
 
109
+ # Returns the permitted attributes for the show action
110
+ # @return [Array<Symbol>]
78
111
  def permitted_attributes_for_show
79
112
  permitted_attributes_for_read
80
113
  end
81
114
 
115
+ # Returns the permitted attributes for the new action
116
+ # @return [Array<Symbol>]
82
117
  def permitted_attributes_for_new
83
118
  permitted_attributes_for_create
84
119
  end
85
120
 
121
+ # Returns the permitted attributes for the edit action
122
+ # @return [Array<Symbol>]
86
123
  def permitted_attributes_for_edit
87
124
  permitted_attributes_for_update
88
125
  end
89
126
 
127
+ # # Returns the permitted associations
128
+ # # @return [Array<Symbol>]
90
129
  # def permitted_associations
91
130
  # []
92
131
  # end
93
132
 
94
133
  private
95
134
 
96
- def autodetect_fields_for(method_name)
97
- maybe_warn_autodetect_usage method_name
135
+ # Autodetects the permitted fields for a given method
136
+ # @param [Symbol] method_name The name of the method
137
+ # @return [Array<Symbol>]
138
+ def autodetect_permitted_fields(method_name)
139
+ warn_about_autodetect_usage(method_name)
98
140
 
99
141
  context.resource_context.resource_class.resource_field_names
100
142
  end
101
143
 
102
- def maybe_warn_autodetect_usage(method)
103
- raise "Resource field auto-detection: #{self.class}##{method} outside development" unless Rails.env.development?
144
+ # Warns about the usage of auto-detection of fields
145
+ # @param [Symbol] method The method name
146
+ # @raise [RuntimeError] if not in development environment
147
+ def warn_about_autodetect_usage(method)
148
+ unless Rails.env.development?
149
+ raise "Resource field auto-detection: #{self.class}##{method} outside development"
150
+ end
104
151
 
105
- Rails.logger.warn %(
152
+ Plutonium.logger.warn %(
106
153
  🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
107
154
 
108
155
  Resource field auto-detection: #{self.class}##{method}
@@ -1,9 +1,14 @@
1
1
  module Plutonium
2
2
  module Resource
3
+ # Presenter class to define actions and fields for a resource
4
+ # @abstract
3
5
  class Presenter
4
6
  include Plutonium::Core::Definers::FieldDefiner
5
7
  include Plutonium::Core::Definers::ActionDefiner
6
8
 
9
+ # Initializes the presenter with context and resource record
10
+ # @param [Object] context The context in which the presenter is used
11
+ # @param [ActiveRecord::Base] resource_record The resource record being presented
7
12
  def initialize(context, resource_record)
8
13
  @context = context
9
14
  @resource_record = resource_record
@@ -17,14 +22,17 @@ module Plutonium
17
22
 
18
23
  attr_reader :context, :resource_record
19
24
 
25
+ # Define fields for the resource
26
+ # @note Override this in child presenters for custom field definitions
20
27
  def define_fields
21
- # override this in child presenters for custom field definitions
22
28
  end
23
29
 
30
+ # Define actions for the resource
31
+ # @note Override this in child presenters for custom action definitions
24
32
  def define_actions
25
- # override this in child presenters for custom action definitions
26
33
  end
27
34
 
35
+ # Define standard actions for the resource
28
36
  def define_standard_actions
29
37
  define_action Plutonium::Core::Actions::NewAction.new(:new)
30
38
  define_action Plutonium::Core::Actions::ShowAction.new(:show)
@@ -32,12 +40,23 @@ module Plutonium
32
40
  define_action Plutonium::Core::Actions::DestroyAction.new(:destroy)
33
41
  end
34
42
 
35
- # TODO: move this to its own definer
43
+ # Define an interactive action
44
+ # @param [Symbol] name The name of the action
45
+ # @param [Object] interaction The interaction object
46
+ # @param [Hash] options Additional options for the action
47
+ # @note This should be moved to its own definer
36
48
  def define_interactive_action(name, interaction:, **)
37
49
  define_action Plutonium::Core::Actions::InteractiveAction.new(name, interaction:, **)
38
50
  end
39
51
 
40
- # TODO: move this to its own definer
52
+ # Define a nested input for the resource
53
+ # @param [Symbol] name The name of the input
54
+ # @param [Array] inputs The inputs for the nested field
55
+ # @param [Class, nil] model_class The model class for the nested field
56
+ # @param [Hash] options Additional options for the nested field
57
+ # @yield [input] Gives the input object to the block
58
+ # @note This should be moved to its own definer
59
+ # @raise [ArgumentError] if model_class is not provided for polymorphic associations
41
60
  def define_nested_input(name, inputs:, model_class: nil, **options)
42
61
  nested_attribute_options = resource_class.all_nested_attributes_options[name]
43
62
 
@@ -50,13 +69,7 @@ module Plutonium
50
69
  macro = nested_attribute_options&.[](:macro)
51
70
  allow_destroy = nested_attribute_options&.[](:allow_destroy).presence
52
71
  update_only = nested_attribute_options&.[](:update_only).presence
53
- limit = if macro == :has_one
54
- 1
55
- elsif options.key?(:limit)
56
- options[:limit]
57
- else
58
- nested_attribute_options&.[](:limit)
59
- end
72
+ limit = determine_nested_input_limit(macro, options[:limit], nested_attribute_options&.[](:limit))
60
73
 
61
74
  input = Plutonium::Core::Fields::Inputs::NestedInput.new(
62
75
  name,
@@ -72,7 +85,26 @@ module Plutonium
72
85
  define_input name, input:
73
86
  end
74
87
 
75
- def resource_class = context.resource_class
88
+ # Determines the limit for a nested input
89
+ # @param [Symbol, nil] macro The macro of the association
90
+ # @param [Integer, nil] option_limit The limit provided in options
91
+ # @param [Integer, nil] nested_attribute_limit The limit from nested attributes
92
+ # @return [Integer, nil] The determined limit
93
+ def determine_nested_input_limit(macro, option_limit, nested_attribute_limit)
94
+ if macro == :has_one
95
+ 1
96
+ elsif option_limit
97
+ option_limit
98
+ else
99
+ nested_attribute_limit
100
+ end
101
+ end
102
+
103
+ # Returns the resource class
104
+ # @return [Class] The resource class
105
+ def resource_class
106
+ context.resource_class
107
+ end
76
108
  end
77
109
  end
78
110
  end
@@ -1,41 +1,61 @@
1
- # TODO: refactor
1
+ # TODO: make standard query type names e.g. search and scope configurable
2
2
 
3
3
  module Plutonium
4
- # TODO: make standard query type names e.g. search and scope configurable
5
4
  module Resource
5
+ # The QueryObject class is responsible for handling various query types and applying them to the given scope.
6
6
  class QueryObject
7
7
  class << self
8
8
  end
9
9
 
10
+ # The Query class serves as a base for different types of queries.
10
11
  class Query
11
12
  include Plutonium::Core::Definers::InputDefiner
12
13
 
14
+ # Applies the query to the given scope with the provided parameters.
15
+ #
16
+ # @param scope [Object] the scope to apply the query on
17
+ # @param params [Hash] the parameters for the query
18
+ # @return [Object] the modified scope
13
19
  def apply(scope, params)
14
- params = extract_query_params params
15
-
16
- if input_definitions.size == params.size
17
- apply_internal scope, params
18
- else
19
- scope
20
- end
20
+ params = extract_query_params(params)
21
+ (input_definitions.size == params.size) ? apply_internal(scope, params) : scope
21
22
  end
22
23
 
23
24
  private
24
25
 
26
+ # Raises an error, should be implemented by subclasses.
27
+ #
28
+ # @param scope [Object] the scope to apply the query on
29
+ # @param params [Hash] the parameters for the query
30
+ # @raise [NotImplementedError] if not implemented by subclass
25
31
  def apply_internal(scope, params)
26
32
  raise NotImplementedError, "#{self.class}#apply_internal"
27
33
  end
28
34
 
35
+ # Extracts and processes query parameters.
36
+ #
37
+ # @param params [Hash] the parameters to process
38
+ # @return [Hash] the processed parameters
29
39
  def extract_query_params(params)
30
- input_definitions.collect_all(params).symbolize_keys
40
+ input_definitions.collect_all(params).compact.symbolize_keys
31
41
  end
32
42
 
33
- def resource_class = nil
43
+ # Returns the resource class, to be implemented by subclasses.
44
+ #
45
+ # @return [Class, nil] the resource class
46
+ def resource_class
47
+ nil
48
+ end
34
49
  end
35
50
 
51
+ # The ScopeQuery class represents a query based on a scope.
36
52
  class ScopeQuery < Query
37
53
  attr_reader :name
38
54
 
55
+ # Initializes a ScopeQuery.
56
+ #
57
+ # @param name [Symbol] the name of the scope
58
+ # @yield [self] optional block to configure the query
39
59
  def initialize(name)
40
60
  @name = name
41
61
  yield self if block_given?
@@ -43,30 +63,47 @@ module Plutonium
43
63
 
44
64
  private
45
65
 
66
+ # Applies the scope query to the given scope with the provided parameters.
67
+ #
68
+ # @param scope [Object] the scope to apply the query on
69
+ # @param params [Hash] the parameters for the query
70
+ # @return [Object] the modified scope
46
71
  def apply_internal(scope, params)
47
- scope.send name, **params
72
+ scope.send(name, **params)
48
73
  end
49
74
  end
50
75
 
76
+ # The BlockQuery class represents a query based on a block.
51
77
  class BlockQuery < Query
52
78
  attr_reader :body
53
79
 
80
+ # Initializes a BlockQuery.
81
+ #
82
+ # @param body [Proc] the block to apply
83
+ # @yield [self] optional block to configure the query
54
84
  def initialize(body)
55
85
  @body = body
56
86
  yield self if block_given?
57
87
  end
58
88
 
89
+ private
90
+
91
+ # Applies the block query to the given scope with the provided parameters.
92
+ #
93
+ # @param scope [Object] the scope to apply the query on
94
+ # @param params [Hash] the parameters for the query
95
+ # @return [Object] the modified scope
59
96
  def apply_internal(scope, params)
60
- if body.arity == 1
61
- body.call scope
62
- else
63
- body.call scope, **params
64
- end
97
+ (body.arity == 1) ? body.call(scope) : body.call(scope, **params)
65
98
  end
66
99
  end
67
100
 
68
- attr_reader :search_filter, :search_query
101
+ attr_reader :search_filter, :search_query, :context, :selected_sort_fields, :selected_sort_directions, :selected_scope_filter
69
102
 
103
+ # Initializes a QueryObject.
104
+ #
105
+ # @param context [Object] the context in which the queries are defined
106
+ # @param params [Hash] the initial parameters for the query object
70
107
  def initialize(context, params)
71
108
  @context = context
72
109
 
@@ -77,58 +114,61 @@ module Plutonium
77
114
 
78
115
  extract_filter_params(params)
79
116
  extract_sort_params(params)
80
- # @params = params.except(:scope, :search, :sort_fields, :sort_directions)
81
117
  end
82
118
 
119
+ # Builds a URL with the current query parameters.
120
+ #
121
+ # @param options [Hash] additional options for building the URL
122
+ # @return [String] the built URL
83
123
  def build_url(**options)
84
124
  q = {}
85
-
86
- q[:search] = options.key?(:search) ? options[:search].presence : search_query
87
- q[:scope] = options.key?(:scope) ? options[:scope].presence : selected_scope_filter
88
-
125
+ q[:search] = options.fetch(:search, search_query).presence
126
+ q[:scope] = options.fetch(:scope, selected_scope_filter).presence
89
127
  q[:sort_directions] = selected_sort_directions.dup
90
128
  q[:sort_fields] = selected_sort_fields.dup
129
+
91
130
  if (sort = options[:sort])
92
- if options[:reset]
93
- q[:sort_fields].delete_if { |e| e == sort.to_s }
94
- q[:sort_directions].delete sort
95
- else
96
- q[:sort_fields] << sort.to_s unless q[:sort_fields].include?(sort.to_s)
97
-
98
- sort_direction = selected_sort_directions[sort]
99
- if sort_direction.nil?
100
- q[:sort_directions][sort] = "ASC"
101
- elsif sort_direction == "ASC"
102
- q[:sort_directions][sort] = "DESC"
103
- else
104
- q[:sort_fields].delete_if { |e| e == sort.to_s }
105
- q[:sort_directions].delete sort
106
- end
107
- end
131
+ handle_sort_options(q, sort, options[:reset])
108
132
  end
109
133
 
110
134
  "?#{{q: q}.to_param}"
111
135
  end
112
136
 
137
+ # Applies the queries to the given scope.
138
+ #
139
+ # @param scope [Object] the scope to apply the queries on
140
+ # @return [Object] the modified scope
113
141
  def apply(scope)
114
142
  scope = search_filter.apply(scope, {search: search_query}) if search_filter.present?
115
143
  scope = scope_definitions[selected_scope_filter].apply(scope, {}) if selected_scope_filter.present?
116
- selected_sort_fields.each do |name|
117
- sorter = sort_definitions[name]
118
- next unless sorter.present?
119
-
120
- params = {direction: selected_sort_directions[name] || "ASC"}
121
- scope = sorter.apply(scope, params)
122
- end
123
- scope
144
+ apply_sorters(scope)
124
145
  end
125
146
 
126
- def scope_definitions = @scope_definitions ||= {}.with_indifferent_access
147
+ # Retrieves the scope definitions.
148
+ #
149
+ # @return [HashWithIndifferentAccess] the scope definitions
150
+ def scope_definitions
151
+ @scope_definitions ||= {}.with_indifferent_access
152
+ end
127
153
 
128
- def filter_definitions = @filter_definitions ||= {}.with_indifferent_access
154
+ # Retrieves the filter definitions.
155
+ #
156
+ # @return [HashWithIndifferentAccess] the filter definitions
157
+ def filter_definitions
158
+ @filter_definitions ||= {}.with_indifferent_access
159
+ end
129
160
 
130
- def sort_definitions = @sort_definitions ||= {}.with_indifferent_access
161
+ # Retrieves the sort definitions.
162
+ #
163
+ # @return [HashWithIndifferentAccess] the sort definitions
164
+ def sort_definitions
165
+ @sort_definitions ||= {}.with_indifferent_access
166
+ end
131
167
 
168
+ # Retrieves the sort parameters for a given name.
169
+ #
170
+ # @param name [Symbol] the name of the sort field
171
+ # @return [Hash, nil] the sort parameters or nil if not defined
132
172
  def sort_params_for(name)
133
173
  return unless sort_definitions[name]
134
174
 
@@ -142,78 +182,151 @@ module Plutonium
142
182
 
143
183
  private
144
184
 
145
- attr_reader :context, :selected_sort_fields, :selected_sort_directions, :selected_scope_filter
146
-
185
+ # Placeholder method for defining filters.
147
186
  def define_filters
148
187
  end
149
188
 
189
+ # Placeholder method for defining scopes.
150
190
  def define_scopes
151
191
  end
152
192
 
193
+ # Placeholder method for defining sorters.
153
194
  def define_sorters
154
195
  end
155
196
 
197
+ # Defines standard queries.
156
198
  def define_standard_queries
157
199
  define_search(:search) if resource_class.respond_to?(:search)
158
200
  end
159
201
 
160
- def define_filter(name, body = nil, &)
202
+ # Defines a filter.
203
+ #
204
+ # @param name [Symbol] the name of the filter
205
+ # @param body [Proc, nil] the body of the filter
206
+ # @yield [Query] optional block to configure the query
207
+ def define_filter(name, body = nil, &block)
161
208
  body ||= name
162
- filter_definitions[name] = build_query(body, &)
209
+ filter_definitions[name] = build_query(body, &block)
163
210
  end
164
211
 
212
+ # Defines a scope.
213
+ #
214
+ # @param name [Symbol] the name of the scope
215
+ # @param body [Proc, nil] the body of the scope
165
216
  def define_scope(name, body = nil)
166
217
  body ||= name
167
218
  scope_definitions[name] = build_query(body)
168
219
  end
169
220
 
221
+ # Defines a sort.
222
+ #
223
+ # @param name [Symbol] the name of the sort field
224
+ # @param body [Proc, nil] the body of the sort
170
225
  def define_sort(name, body = nil)
171
226
  if body.nil?
172
- sort_field = if resource_class.primary_key == name.to_s || resource_class.content_column_field_names.include?(name)
173
- name
174
- elsif resource_class.belongs_to_association_field_names.include? name
175
- resource_class.reflect_on_association(name).foreign_key.to_sym
176
- else
177
- raise "Unable to determine sort logic for '#{body}'"
178
- end
179
- body = lambda { |scope, direction:| scope.order(sort_field => direction) }
227
+ sort_field = determine_sort_field(name)
228
+ body = ->(scope, direction:) { scope.order(sort_field => direction) }
180
229
  end
181
-
182
230
  sort_definitions[name] = build_query(body) do |query|
183
231
  query.define_input :direction
184
232
  end
185
233
  end
186
234
 
235
+ # Defines a search filter.
236
+ #
237
+ # @param body [Proc] the body of the search filter
187
238
  def define_search(body)
188
239
  @search_filter = build_query(body) do |query|
189
240
  query.define_input :search
190
241
  end
191
242
  end
192
243
 
244
+ # Extracts filter parameters from the given params.
245
+ #
246
+ # @param params [Hash] the parameters to extract from
193
247
  def extract_filter_params(params)
194
248
  @search_query = params[:search]
195
249
  @selected_scope_filter = params[:scope]
196
250
  end
197
251
 
252
+ # Extracts sort parameters from the given params.
253
+ #
254
+ # @param params [Hash] the parameters to extract from
198
255
  def extract_sort_params(params)
199
- @selected_sort_fields = Array(params[:sort_fields])
200
- @selected_sort_fields &= sort_definitions.keys
201
-
202
- @selected_sort_directions = params[:sort_directions]&.slice(*sort_definitions.keys) || {}
203
- @selected_sort_directions = @selected_sort_directions.map { |key, value| [key, {"DESC" => "DESC"}[value.upcase] || "ASC"] }.to_h.with_indifferent_access
256
+ @selected_sort_fields = Array(params[:sort_fields]) & sort_definitions.keys
257
+ @selected_sort_directions = (params[:sort_directions]&.slice(*sort_definitions.keys) || {}).transform_values { |v| (v.upcase == "DESC") ? "DESC" : "ASC" }.with_indifferent_access
204
258
  end
205
259
 
206
- def build_query(body, &)
260
+ # Builds a query object.
261
+ #
262
+ # @param body [Symbol, Proc] the body of the query
263
+ # @yield [Query] optional block to configure the query
264
+ # @return [Query] the built query object
265
+ def build_query(body, &block)
207
266
  case body
208
267
  when Symbol
209
- raise "Cannot find scope :#{body} on #{resource_class}" unless resource_class.respond_to? body
210
- ScopeQuery.new(body, &)
268
+ raise "Cannot find scope :#{body} on #{resource_class}" unless resource_class.respond_to?(body)
269
+ ScopeQuery.new(body, &block)
270
+ else
271
+ BlockQuery.new(body, &block)
272
+ end
273
+ end
274
+
275
+ # Retrieves the resource class.
276
+ #
277
+ # @return [Class] the resource class
278
+ def resource_class
279
+ context.resource_class
280
+ end
281
+
282
+ # Determines the sort field for a given name.
283
+ #
284
+ # @param name [Symbol] the name of the sort field
285
+ # @return [Symbol] the determined sort field
286
+ # @raise [RuntimeError] if unable to determine sort logic for the field
287
+ def determine_sort_field(name)
288
+ if resource_class.primary_key == name.to_s || resource_class.content_column_field_names.include?(name)
289
+ name
290
+ elsif resource_class.belongs_to_association_field_names.include?(name)
291
+ resource_class.reflect_on_association(name).foreign_key.to_sym
292
+ else
293
+ raise "Unable to determine sort logic for '#{name}'"
294
+ end
295
+ end
296
+
297
+ # Handles sorting options.
298
+ #
299
+ # @param query [Hash] the query parameters
300
+ # @param sort [Symbol] the sort field
301
+ # @param reset [Boolean] whether to reset the sorting
302
+ def handle_sort_options(query, sort, reset)
303
+ if reset
304
+ query[:sort_fields].delete_if { |e| e == sort.to_s }
305
+ query[:sort_directions].delete(sort)
211
306
  else
212
- BlockQuery.new(body, &)
307
+ query[:sort_fields] << sort.to_s unless query[:sort_fields].include?(sort.to_s)
308
+ sort_direction = selected_sort_directions[sort]
309
+ query[:sort_directions][sort] = if sort_direction.nil?
310
+ "ASC"
311
+ else
312
+ ((sort_direction == "ASC") ? "DESC" : "ASC")
313
+ end
314
+ query[:sort_fields].delete_if { |e| e == sort.to_s } if query[:sort_directions][sort] == "ASC"
213
315
  end
214
316
  end
215
317
 
216
- def resource_class = context.resource_class
318
+ # Applies sorters to the scope.
319
+ #
320
+ # @param scope [Object] the scope to apply sorters on
321
+ # @return [Object] the modified scope
322
+ def apply_sorters(scope)
323
+ selected_sort_fields.each do |name|
324
+ sorter = sort_definitions[name]
325
+ next unless sorter.present?
326
+ scope = sorter.apply(scope, {direction: selected_sort_directions[name] || "ASC"})
327
+ end
328
+ scope
329
+ end
217
330
  end
218
331
  end
219
332
  end