plutonium 0.10.1 → 0.10.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -0
- data/app/assets/javascripts/turbo/index.js +1 -1
- data/app/views/application/_resource_header.html.erb +12 -12
- data/app/views/layouts/resource.html.erb +1 -0
- data/app/views/layouts/rodauth.html.erb +4 -4
- data/app/views/resource/_nav_user.html.erb +1 -1
- data/app/views/resource/index.rabl +1 -1
- data/brakeman.ignore +1 -1
- data/config/initializers/rabl.rb +2 -0
- data/css.manifest +1 -1
- data/js.manifest +2 -2
- data/lib/generators/pu/core/install/templates/config/initializers/plutonium.rb +0 -3
- data/lib/generators/pu/gen/pug/pug_generator.rb +6 -0
- data/lib/generators/pu/gen/pug/templates/pug.rb.tt +1 -1
- data/lib/generators/pu/pkg/app/templates/config/routes.rb.tt +3 -3
- data/lib/plutonium/config.rb +0 -11
- data/lib/plutonium/core/autodiscovery/input_discoverer.rb +1 -1
- data/lib/plutonium/core/autodiscovery/renderer_discoverer.rb +1 -1
- data/lib/plutonium/core/controllers/base.rb +13 -3
- data/lib/plutonium/core/controllers/queryable.rb +3 -1
- data/lib/plutonium/pkg/app.rb +6 -0
- data/lib/plutonium/railtie.rb +15 -0
- data/lib/plutonium/reloader.rb +99 -0
- data/lib/plutonium/resource/controller.rb +61 -22
- data/lib/plutonium/resource/policy.rb +56 -9
- data/lib/plutonium/resource/presenter.rb +44 -12
- data/lib/plutonium/resource/query_object.rb +186 -73
- data/lib/plutonium/resource/record.rb +213 -119
- data/lib/plutonium/rodauth/controller_methods.rb +7 -0
- data/lib/plutonium/version.rb +1 -1
- data/lib/plutonium.rb +50 -12
- data/package-lock.json +174 -0
- data/package.json +2 -0
- data/public/plutonium-assets/plutonium-app-6WILQCTT.js +39 -0
- data/public/plutonium-assets/plutonium-app-6WILQCTT.js.map +7 -0
- data/public/plutonium-assets/plutonium-logo-original.png +0 -0
- data/public/plutonium-assets/plutonium-logo-white.png +0 -0
- data/public/plutonium-assets/plutonium-logo.png +0 -0
- data/public/plutonium-assets/plutonium.2d4f0c333cd000051d3b.css +3424 -0
- data/public/plutonium-assets/plutonium.ico +0 -0
- metadata +10 -4
- data/lib/plutonium/reactor/core.rb +0 -78
- 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
|
-
|
13
|
+
unless respond_to?(method)
|
14
|
+
raise NotImplementedError, "#{self.class.name} does not implement the required #{method}"
|
15
|
+
end
|
11
16
|
|
12
|
-
send
|
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
|
-
|
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
|
-
|
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
|
-
|
97
|
-
|
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
|
-
|
103
|
-
|
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
|
-
|
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
|
-
#
|
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
|
-
#
|
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 =
|
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
|
-
|
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:
|
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
|
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
|
-
|
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
|
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
|
-
|
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[:
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
173
|
-
|
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
|
-
@
|
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
|
-
|
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?
|
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
|
-
|
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
|
-
|
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
|