plutonium 0.10.1 → 0.10.2

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 (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 +0 -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