rest_framework 1.2.0 → 2.0.0.beta1
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.
- checksums.yaml +4 -4
- data/README.md +107 -41
- data/VERSION +1 -1
- data/lib/rest_framework/controller/actions.rb +256 -0
- data/lib/rest_framework/controller/bulk.rb +15 -10
- data/lib/rest_framework/controller/crud.rb +10 -5
- data/lib/rest_framework/controller/openapi.rb +3 -2
- data/lib/rest_framework/controller.rb +194 -118
- data/lib/rest_framework/errors.rb +18 -1
- data/lib/rest_framework/filters/base_filter.rb +10 -0
- data/lib/rest_framework/filters/ordering_filter.rb +41 -22
- data/lib/rest_framework/filters/query_filter.rb +17 -3
- data/lib/rest_framework/filters/search_filter.rb +4 -0
- data/lib/rest_framework/paginators/page_number_paginator.rb +25 -9
- data/lib/rest_framework/routers.rb +52 -188
- data/lib/rest_framework/serializers/active_model_serializer_adapter_factory.rb +2 -2
- data/lib/rest_framework/serializers/base_serializer.rb +2 -2
- data/lib/rest_framework/serializers/native_serializer.rb +76 -24
- data/lib/rest_framework/utils.rb +32 -70
- data/lib/rest_framework/version.rb +8 -5
- data/lib/rest_framework.rb +7 -38
- metadata +5 -8
- data/lib/rest_framework/mixins/base_controller_mixin.rb +0 -12
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +0 -55
- data/lib/rest_framework/mixins/model_controller_mixin.rb +0 -110
- data/lib/rest_framework/mixins.rb +0 -7
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators::BasePaginator
|
|
5
5
|
def initialize(**kwargs)
|
|
6
6
|
super
|
|
7
|
-
# Exclude any `select` clauses since that would cause `count` to fail with a SQL `SyntaxError`.
|
|
8
|
-
@count = @data.except(:select).count
|
|
9
7
|
@page_size = self._page_size
|
|
8
|
+
@total_count = @controller.class.page_total_count
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
# Compute the total count (and total pages) unless disabled. On large tables `page_total_count`
|
|
11
|
+
# can be set to `false` to skip this `COUNT(*)` over the whole filtered set; `next` is then
|
|
12
|
+
# derived by fetching one extra record in `get_page`.
|
|
13
|
+
if @total_count
|
|
14
|
+
# Exclude any `select` clauses, since that would cause `count` to fail with a SQL
|
|
15
|
+
# `SyntaxError`.
|
|
16
|
+
@count = @data.except(:select).count
|
|
17
|
+
@total_pages = @count / @page_size
|
|
18
|
+
@total_pages += 1 if @count % @page_size != 0
|
|
19
|
+
end
|
|
13
20
|
end
|
|
14
21
|
|
|
15
22
|
def _page_size
|
|
@@ -17,7 +24,7 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
|
17
24
|
|
|
18
25
|
# Get from query param, if allowed.
|
|
19
26
|
if param = @controller.class.page_size_query_param
|
|
20
|
-
if raw = @controller.
|
|
27
|
+
if raw = @controller.request.query_parameters[param].presence
|
|
21
28
|
parsed = raw.to_i
|
|
22
29
|
page_size = parsed if parsed > 0
|
|
23
30
|
end
|
|
@@ -38,9 +45,9 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
|
38
45
|
|
|
39
46
|
# Get the page and return it so the caller can serialize it.
|
|
40
47
|
def get_page(page_number = nil)
|
|
41
|
-
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
|
48
|
+
# If page number isn't provided, infer from the query params or use 1 as a fallback value.
|
|
42
49
|
unless page_number
|
|
43
|
-
page_number = @controller&.
|
|
50
|
+
page_number = @controller&.request&.query_parameters&.[](@controller.class.page_query_param)
|
|
44
51
|
if page_number.blank?
|
|
45
52
|
page_number = 1
|
|
46
53
|
else
|
|
@@ -54,14 +61,23 @@ class RESTFramework::Paginators::PageNumberPaginator < RESTFramework::Paginators
|
|
|
54
61
|
|
|
55
62
|
# Get the data page and return it so the caller can serialize the data in the proper format.
|
|
56
63
|
page_index = @page_number - 1
|
|
57
|
-
|
|
64
|
+
offset = page_index * @page_size
|
|
65
|
+
|
|
66
|
+
# Without a total count we can't derive `next` from `total_pages`, so detect whether a further
|
|
67
|
+
# page exists with a cheap existence check (a `LIMIT 1` past this page) instead of a full count.
|
|
68
|
+
unless @total_count
|
|
69
|
+
@has_next = @data.except(:select).offset(offset + @page_size).exists?
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
@data.limit(@page_size).offset(offset)
|
|
58
73
|
end
|
|
59
74
|
|
|
60
75
|
# Wrap the serialized page with appropriate metadata.
|
|
61
76
|
def get_paginated_response(serialized_page)
|
|
62
77
|
page_query_param = @controller.class.page_query_param
|
|
63
78
|
base_params = @controller.request.query_parameters.symbolize_keys
|
|
64
|
-
|
|
79
|
+
has_next = @total_count ? @page_number < @total_pages : @has_next
|
|
80
|
+
next_url = if has_next
|
|
65
81
|
@controller.url_for({ **base_params, page_query_param => @page_number + 1 })
|
|
66
82
|
end
|
|
67
83
|
previous_url = if @page_number > 1
|
|
@@ -2,221 +2,85 @@ require "action_dispatch/routing/mapper"
|
|
|
2
2
|
|
|
3
3
|
module ActionDispatch::Routing
|
|
4
4
|
class Mapper
|
|
5
|
-
#
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
name
|
|
10
|
-
if name == name.pluralize
|
|
11
|
-
name_reverse = name.singularize
|
|
12
|
-
else
|
|
13
|
-
name_reverse = name.pluralize
|
|
14
|
-
end
|
|
15
|
-
name += "Controller"
|
|
16
|
-
name_reverse += "Controller"
|
|
17
|
-
|
|
18
|
-
# Get scope for the class.
|
|
19
|
-
if @scope[:module]
|
|
20
|
-
mod = @scope[:module].to_s.camelize.constantize
|
|
21
|
-
else
|
|
22
|
-
mod = Object
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
# Convert class name to class.
|
|
26
|
-
begin
|
|
27
|
-
controller = mod.const_get(name)
|
|
28
|
-
rescue NameError
|
|
29
|
-
if fallback_reverse_pluralization
|
|
30
|
-
reraise = false
|
|
31
|
-
|
|
32
|
-
begin
|
|
33
|
-
controller = mod.const_get(name_reverse)
|
|
34
|
-
rescue NameError
|
|
35
|
-
reraise = true
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
if reraise
|
|
39
|
-
raise
|
|
40
|
-
end
|
|
41
|
-
else
|
|
42
|
-
raise
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
controller
|
|
5
|
+
# Resolve a controller class from a route name and the current scope. The name must match the
|
|
6
|
+
# controller exactly (camelized, plus `Controller`) — there is no pluralization fallback.
|
|
7
|
+
def _rrf_controller_class(name)
|
|
8
|
+
mod = @scope[:module] ? @scope[:module].to_s.camelize.constantize : Object
|
|
9
|
+
mod.const_get("#{name.to_s.camelize}Controller")
|
|
47
10
|
end
|
|
48
11
|
|
|
49
|
-
#
|
|
50
|
-
def
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
12
|
+
# Route each action from a controller's action store.
|
|
13
|
+
def _rrf_route_actions(actions)
|
|
14
|
+
actions.each_value do |spec|
|
|
15
|
+
# Delegated actions keep their declared action name (so routing and OpenAPI show the real
|
|
16
|
+
# name); `method_for_action` redirects dispatch to `rrf_delegate`, which needs the scope.
|
|
17
|
+
kwargs = spec.kwargs
|
|
18
|
+
if !spec.builtin && spec.metadata&.[](:delegate)
|
|
19
|
+
kwargs = kwargs.merge(rrf_delegate_scope: spec.type)
|
|
56
20
|
end
|
|
57
21
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
key = "#{@scope[:path]}/#{config[:path]}"
|
|
61
|
-
RESTFramework::EXTRA_ACTION_ROUTES.add(key)
|
|
62
|
-
RESTFramework::ROUTE_METADATA[key] = metadata if metadata
|
|
63
|
-
|
|
64
|
-
yield if block_given?
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Internal core implementation of the `rest_resource(s)` router, both singular and plural.
|
|
69
|
-
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
|
70
|
-
# not otherwise defined by the controller
|
|
71
|
-
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
|
72
|
-
def _rest_resources(default_singular, name, **kwargs, &block)
|
|
73
|
-
controller = kwargs.delete(:controller) || name
|
|
74
|
-
if controller.is_a?(Class)
|
|
75
|
-
controller_class = controller
|
|
76
|
-
else
|
|
77
|
-
controller_class = self._get_controller_class(controller, pluralize: !default_singular)
|
|
78
|
-
end
|
|
79
|
-
|
|
80
|
-
# Set controller if it's not explicitly set.
|
|
81
|
-
kwargs[:controller] = name unless kwargs[:controller]
|
|
82
|
-
|
|
83
|
-
# Passing `unscoped: true` will prevent a nested resource from being scoped.
|
|
84
|
-
unscoped = kwargs.delete(:unscoped)
|
|
85
|
-
|
|
86
|
-
# Determine plural/singular resource.
|
|
87
|
-
if !controller_class.singleton_controller.nil?
|
|
88
|
-
singular = controller_class.singleton_controller
|
|
89
|
-
else
|
|
90
|
-
singular = default_singular
|
|
91
|
-
end
|
|
92
|
-
resource_method = singular ? :resource : :resources
|
|
93
|
-
|
|
94
|
-
# Call either `resource` or `resources`, passing appropriate modifiers.
|
|
95
|
-
skip = RESTFramework::Utils.get_skipped_builtin_actions(controller_class, singular)
|
|
96
|
-
public_send(resource_method, name, except: skip, **kwargs) do
|
|
97
|
-
if controller_class.respond_to?(:extra_member_actions)
|
|
98
|
-
member do
|
|
99
|
-
self._route_extra_actions(controller_class.extra_member_actions)
|
|
100
|
-
end
|
|
22
|
+
spec.methods.each do |m|
|
|
23
|
+
public_send(m, spec.path, action: spec.name, **kwargs)
|
|
101
24
|
end
|
|
102
25
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
self._route_extra_actions(controller_class.extra_actions)
|
|
106
|
-
|
|
107
|
-
# Route extra RRF-defined actions.
|
|
108
|
-
RESTFramework::RRF_BUILTIN_ACTIONS.each do |action, methods|
|
|
109
|
-
next unless controller_class.method_defined?(action)
|
|
110
|
-
|
|
111
|
-
[ methods ].flatten.each do |m|
|
|
112
|
-
# Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
|
|
113
|
-
# causes parent OPTIONS routes to greedily intercept sub-path requests.
|
|
114
|
-
public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
|
|
115
|
-
end
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
# Route bulk actions, if configured. These require a model and are gated by the `bulk`
|
|
119
|
-
# attribute, and may be individually excluded via `excluded_actions`.
|
|
120
|
-
if controller_class.model && controller_class.bulk
|
|
121
|
-
bulk_exclude = controller_class.excluded_actions&.to_set || Set.new
|
|
122
|
-
RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
|
|
123
|
-
next unless controller_class.method_defined?(action)
|
|
124
|
-
next if bulk_exclude.include?(action)
|
|
26
|
+
# Record non-builtin (extra) actions and their metadata for the browsable API / OpenAPI.
|
|
27
|
+
next if spec.builtin
|
|
125
28
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
if unscoped
|
|
136
|
-
yield if block_given?
|
|
137
|
-
else
|
|
138
|
-
scope(module: name, as: name) do
|
|
139
|
-
yield if block_given?
|
|
140
|
-
end
|
|
141
|
-
end
|
|
29
|
+
key = "#{@scope[:path]}/#{spec.path}"
|
|
30
|
+
RESTFramework::EXTRA_ACTION_ROUTES.add(key)
|
|
31
|
+
RESTFramework::ROUTE_METADATA[key] = spec.metadata if spec.metadata
|
|
142
32
|
end
|
|
143
33
|
end
|
|
144
34
|
|
|
145
|
-
#
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
35
|
+
# Route one or more controllers from their action stores. Plural model controllers get
|
|
36
|
+
# collection/member scopes; singular and non-model controllers route everything at the root.
|
|
37
|
+
# Passing several names condenses simple routes into one call; per-name options (`path:`, `as:`,
|
|
38
|
+
# `controller:`, and a block) only apply to a single name.
|
|
39
|
+
def rest_route(*names, **kwargs, &block)
|
|
40
|
+
if names.size > 1 && (block || (kwargs.keys & [ :path, :as, :controller ]).any?)
|
|
41
|
+
raise ArgumentError, "rest_route: options and a block require a single name"
|
|
149
42
|
end
|
|
150
|
-
end
|
|
151
43
|
|
|
152
|
-
|
|
153
|
-
def rest_resources(*names, **kwargs, &block)
|
|
154
|
-
names.each do |n|
|
|
155
|
-
self._rest_resources(false, n, **kwargs, &block)
|
|
156
|
-
end
|
|
44
|
+
names.each { |name| _rrf_rest_route(name, **kwargs, &block) }
|
|
157
45
|
end
|
|
158
46
|
|
|
159
|
-
# Route a controller
|
|
160
|
-
def
|
|
47
|
+
# Route a single controller from its action store.
|
|
48
|
+
def _rrf_rest_route(name, **kwargs)
|
|
161
49
|
controller = kwargs.delete(:controller) || name
|
|
162
|
-
route_root_to = kwargs.delete(:route_root_to)
|
|
163
50
|
if controller.is_a?(Class)
|
|
164
51
|
controller_class = controller
|
|
165
52
|
else
|
|
166
|
-
controller_class = self.
|
|
53
|
+
controller_class = self._rrf_controller_class(controller)
|
|
167
54
|
end
|
|
168
55
|
|
|
169
56
|
# Set controller if it's not explicitly set.
|
|
170
57
|
kwargs[:controller] = name unless kwargs[:controller]
|
|
171
58
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Anchor the route since Rails 8.1 OPTIONS routes are non-anchored by default, which
|
|
192
|
-
# causes parent OPTIONS routes to greedily intercept sub-path requests.
|
|
193
|
-
public_send(m, "", action: action, anchor: true) if self.respond_to?(m)
|
|
194
|
-
end
|
|
59
|
+
has_model = !!controller_class.model
|
|
60
|
+
singular = controller_class.singular
|
|
61
|
+
actions = controller_class.actions
|
|
62
|
+
member_actions = controller_class.member_actions
|
|
63
|
+
|
|
64
|
+
# Use `resources` (plural) for plural model controllers to get the member `:id` scope; use
|
|
65
|
+
# `resource` (singular) for everything else.
|
|
66
|
+
resource_method = (has_model && !singular) ? :resources : :resource
|
|
67
|
+
|
|
68
|
+
public_send(resource_method, name, only: [], **kwargs) do
|
|
69
|
+
if has_model
|
|
70
|
+
if singular
|
|
71
|
+
# Singular model controller: actions and member actions are the same.
|
|
72
|
+
self._rrf_route_actions(actions)
|
|
73
|
+
self._rrf_route_actions(member_actions)
|
|
74
|
+
else
|
|
75
|
+
# Plural model controller: route collection/member actions separately.
|
|
76
|
+
collection { self._rrf_route_actions(actions) }
|
|
77
|
+
member { self._rrf_route_actions(member_actions) }
|
|
195
78
|
end
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
if unscoped
|
|
199
|
-
yield if block_given?
|
|
200
79
|
else
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
end
|
|
80
|
+
# Non-model controller: only actions (there is no member `:id` scope).
|
|
81
|
+
self._rrf_route_actions(actions)
|
|
204
82
|
end
|
|
205
|
-
end
|
|
206
|
-
end
|
|
207
|
-
|
|
208
|
-
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
|
209
|
-
def rest_root(name = nil, **kwargs, &block)
|
|
210
|
-
# By default, use RootController#root.
|
|
211
|
-
root_action = kwargs.delete(:action) || :root
|
|
212
|
-
controller = kwargs.delete(:controller) || name || :root
|
|
213
|
-
|
|
214
|
-
# Remove path if name is nil (routing to the root of current namespace).
|
|
215
|
-
unless name
|
|
216
|
-
kwargs[:path] = ""
|
|
217
|
-
end
|
|
218
83
|
|
|
219
|
-
rest_route(controller, route_root_to: root_action, **kwargs) do
|
|
220
84
|
yield if block_given?
|
|
221
85
|
end
|
|
222
86
|
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# This is a helper factory to wrap an ActiveModelSerializer to provide a `serialize` method which
|
|
2
2
|
# accepts both collections and individual records. Use `.for` to build adapters.
|
|
3
|
-
# :
|
|
3
|
+
# simplecov:disable
|
|
4
4
|
class RESTFramework::Serializers::ActiveModelSerializerAdapterFactory
|
|
5
5
|
def self.for(active_model_serializer)
|
|
6
6
|
Class.new(active_model_serializer) do
|
|
@@ -14,7 +14,7 @@ class RESTFramework::Serializers::ActiveModelSerializerAdapterFactory
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
-
# :
|
|
17
|
+
# simplecov:enable
|
|
18
18
|
|
|
19
19
|
# Alias for convenience.
|
|
20
20
|
# rubocop:disable Layout/LineLength
|
|
@@ -16,7 +16,7 @@ class RESTFramework::Serializers::BaseSerializer
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
# Synonym for `serialize` for compatibility with `active_model_serializers`.
|
|
19
|
-
# :
|
|
19
|
+
# simplecov:disable
|
|
20
20
|
def serializable_hash(*args)
|
|
21
21
|
self.serialize(*args)
|
|
22
22
|
end
|
|
@@ -35,7 +35,7 @@ class RESTFramework::Serializers::BaseSerializer
|
|
|
35
35
|
def associations(*args, **kwargs)
|
|
36
36
|
[]
|
|
37
37
|
end
|
|
38
|
-
# :
|
|
38
|
+
# simplecov:enable
|
|
39
39
|
end
|
|
40
40
|
|
|
41
41
|
# Alias for convenience.
|
|
@@ -111,25 +111,77 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
111
111
|
controller_serializer || @controller.class.native_serializer_config
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
114
|
+
# The record cap for a collection association (`nil` = unlimited). The default is applied even
|
|
115
|
+
# when the feature is off, so responses are always bounded. `key?` (not `||`) reads the
|
|
116
|
+
# `field_config` override so an explicit `nil` there means unlimited/uncapped rather than falling
|
|
117
|
+
# back to the controller default.
|
|
118
|
+
def _effective_association_limit(association_name, field_config)
|
|
119
|
+
controller = @controller&.class
|
|
120
|
+
|
|
121
|
+
default = field_config.key?(:limit) ?
|
|
122
|
+
field_config[:limit] : controller&.association_limit
|
|
123
|
+
return default unless controller&.enable_association_queries
|
|
124
|
+
|
|
125
|
+
requested = self._requested_association_limit(association_name)
|
|
126
|
+
return default if requested.nil?
|
|
127
|
+
|
|
128
|
+
max = field_config.key?(:limit_max) ?
|
|
129
|
+
field_config[:limit_max] : controller.association_limit_max
|
|
130
|
+
|
|
131
|
+
# `all` means "as many as allowed" — the cap, or unlimited when the cap is `nil`.
|
|
132
|
+
return max if requested == :all
|
|
133
|
+
|
|
134
|
+
max ? [ requested, max ].min : requested
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# `all`, `none`, and `0` all mean "no limit" — `0` is free to reuse as a sentinel since an
|
|
138
|
+
# association is dropped via `except`, not `limit=0`. Anything else must be a plain positive
|
|
139
|
+
# integer, so a nested/array param or junk can't drive the query.
|
|
140
|
+
def _requested_association_limit(association_name)
|
|
141
|
+
return nil unless prefix = @controller.class.association_query_prefix.presence
|
|
142
|
+
|
|
143
|
+
raw = @controller.request&.query_parameters&.[]("#{prefix}.#{association_name}.limit")
|
|
144
|
+
return nil unless raw.is_a?(String)
|
|
145
|
+
|
|
146
|
+
raw = raw.strip
|
|
147
|
+
return :all if raw.in?(%w[all none])
|
|
148
|
+
return nil unless raw.match?(/\A\d+\z/)
|
|
149
|
+
|
|
150
|
+
# The regex guarantees a non-negative integer, so anything but zero is a positive limit.
|
|
151
|
+
limit = raw.to_i
|
|
152
|
+
limit.zero? ? :all : limit
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# The fields to serialize for association `f`, honoring a consumer's `?<prefix>.<f>.fields=`
|
|
156
|
+
# request bounded by the allowlist. Only active when the feature is enabled.
|
|
157
|
+
def _effective_association_fields(association_name, ref, field_config)
|
|
158
|
+
default_fields = field_config[:fields]
|
|
159
|
+
return default_fields unless @controller&.class&.enable_association_queries
|
|
160
|
+
return default_fields if ref.polymorphic? # no single target class to bound the request
|
|
161
|
+
|
|
162
|
+
requested = self._requested_association_fields(association_name)
|
|
163
|
+
return default_fields if requested.blank?
|
|
164
|
+
|
|
165
|
+
# The primary key is always kept so records stay identifiable. `_valid_association_field?` is
|
|
166
|
+
# the last line of defense against leaking a non-serializable or nested-association field.
|
|
167
|
+
allowed = (default_fields + (field_config[:requestable_fields] || [])).uniq
|
|
168
|
+
valid = (requested & allowed).select { |sf| self._valid_association_field?(ref, sf) }
|
|
169
|
+
(Array(ref.klass.primary_key).map(&:to_s) + valid).uniq
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# Only a plain scalar string is honored, so a nested-hash/array param can't reach `where`/`split`.
|
|
173
|
+
def _requested_association_fields(association_name)
|
|
174
|
+
return nil unless prefix = @controller.class.association_query_prefix.presence
|
|
175
|
+
|
|
176
|
+
raw = @controller.request&.query_parameters&.[]("#{prefix}.#{association_name}.fields")
|
|
177
|
+
return nil unless raw.is_a?(String)
|
|
178
|
+
|
|
179
|
+
raw.split(",").map { |x| x.strip.presence }.compact
|
|
180
|
+
end
|
|
131
181
|
|
|
132
|
-
|
|
182
|
+
def _valid_association_field?(ref, field)
|
|
183
|
+
field.in?(ref.klass.column_names) ||
|
|
184
|
+
(ref.klass.method_defined?(field) && !ref.klass.reflect_on_association(field.to_sym))
|
|
133
185
|
end
|
|
134
186
|
|
|
135
187
|
# Get a serializer configuration from the controller. `@controller` and `@model` must be set.
|
|
@@ -158,7 +210,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
158
210
|
elsif ref = reflections[f]
|
|
159
211
|
sub_columns = []
|
|
160
212
|
sub_methods = []
|
|
161
|
-
field_config
|
|
213
|
+
self._effective_association_fields(f, ref, field_config).each do |sf|
|
|
162
214
|
if !ref.polymorphic? && sf.in?(ref.klass.column_names)
|
|
163
215
|
sub_columns << sf
|
|
164
216
|
else
|
|
@@ -169,9 +221,9 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
169
221
|
|
|
170
222
|
# Apply certain rules regarding collection associations.
|
|
171
223
|
if ref.collection?
|
|
172
|
-
#
|
|
173
|
-
#
|
|
174
|
-
if limit = self.
|
|
224
|
+
# A finite limit needs a per-record `.limit` query, since eager `includes` can't cap rows
|
|
225
|
+
# per parent; an unlimited (`nil`) association falls through to `includes` preloading.
|
|
226
|
+
if limit = self._effective_association_limit(f, field_config)
|
|
175
227
|
serializer_methods[f] = f
|
|
176
228
|
self.define_singleton_method(f) do |record|
|
|
177
229
|
next record.send(f).limit(limit).as_json(**sub_config)
|
|
@@ -181,7 +233,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
181
233
|
#
|
|
182
234
|
# # Even though we use a serializer method, if the count will later be added, then put
|
|
183
235
|
# # this field into the includes_map.
|
|
184
|
-
# if @controller.class.
|
|
236
|
+
# if @controller.class.include_association_count
|
|
185
237
|
# includes_map[f] = f.to_sym
|
|
186
238
|
# end
|
|
187
239
|
else
|
|
@@ -190,7 +242,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
|
|
|
190
242
|
end
|
|
191
243
|
|
|
192
244
|
# If we need to include the association count, then add it here.
|
|
193
|
-
if @controller.class.
|
|
245
|
+
if @controller.class.include_association_count
|
|
194
246
|
method_name = "#{f}.count"
|
|
195
247
|
serializer_methods[method_name] = method_name
|
|
196
248
|
self.define_singleton_method(method_name) do |record|
|
data/lib/rest_framework/utils.rb
CHANGED
|
@@ -1,59 +1,6 @@
|
|
|
1
1
|
module RESTFramework::Utils
|
|
2
2
|
HTTP_VERB_ORDERING = %w[GET POST PUT PATCH DELETE OPTIONS HEAD]
|
|
3
3
|
|
|
4
|
-
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, metadata:, kwargs:}`.
|
|
5
|
-
def self.parse_extra_actions(extra_actions)
|
|
6
|
-
(extra_actions || {}).map { |k, v|
|
|
7
|
-
path = k
|
|
8
|
-
kwargs = {}
|
|
9
|
-
|
|
10
|
-
# Convert structure to path/methods/kwargs.
|
|
11
|
-
if v.is_a?(Hash)
|
|
12
|
-
# Symbolize keys (which also makes a copy so we don't mutate the original).
|
|
13
|
-
v = v.symbolize_keys
|
|
14
|
-
|
|
15
|
-
# Cast method/methods to an array.
|
|
16
|
-
methods = [ v.delete(:methods), v.delete(:method) ].flatten.compact
|
|
17
|
-
|
|
18
|
-
# Override path if it's provided.
|
|
19
|
-
if v.key?(:path)
|
|
20
|
-
path = v.delete(:path)
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Extract metadata, if provided.
|
|
24
|
-
metadata = v.delete(:metadata).presence
|
|
25
|
-
|
|
26
|
-
# Pass any further kwargs to the underlying Rails interface.
|
|
27
|
-
kwargs = v
|
|
28
|
-
else
|
|
29
|
-
methods = [ v ].flatten
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
next [
|
|
33
|
-
k,
|
|
34
|
-
{
|
|
35
|
-
path: path,
|
|
36
|
-
methods: methods,
|
|
37
|
-
metadata: metadata,
|
|
38
|
-
kwargs: kwargs,
|
|
39
|
-
}.compact,
|
|
40
|
-
]
|
|
41
|
-
}.to_h
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def self.get_skipped_builtin_actions(controller_class, singular)
|
|
45
|
-
candidates = (
|
|
46
|
-
RESTFramework::BUILTIN_ACTIONS.keys - (singular ? [ :index ] : [])
|
|
47
|
-
) + RESTFramework::BUILTIN_MEMBER_ACTIONS.keys
|
|
48
|
-
|
|
49
|
-
return candidates unless controller_class.model
|
|
50
|
-
|
|
51
|
-
exclude = controller_class.excluded_actions&.to_set || Set.new
|
|
52
|
-
candidates.reject do |action|
|
|
53
|
-
controller_class.method_defined?(action) && !exclude.include?(action)
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
|
|
57
4
|
# Get the first route pattern which matches the given request.
|
|
58
5
|
def self.get_request_route(application_routes, request)
|
|
59
6
|
# Prefer the route already resolved by the router to avoid an expensive `recognize` call. This
|
|
@@ -172,9 +119,8 @@ module RESTFramework::Utils
|
|
|
172
119
|
parsed_fields.map(&:to_s)
|
|
173
120
|
end
|
|
174
121
|
|
|
175
|
-
# Get the fields for a given model, including not just columns (which includes
|
|
176
|
-
#
|
|
177
|
-
# strings, not symbols.
|
|
122
|
+
# Get the fields for a given model, including not just columns (which includes foreign keys), but
|
|
123
|
+
# also associations. Note that we always return an array of strings, not symbols.
|
|
178
124
|
def self.fields_for(model, exclude_associations:, action_text:, active_storage:)
|
|
179
125
|
foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
|
|
180
126
|
base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
|
|
@@ -208,23 +154,23 @@ module RESTFramework::Utils
|
|
|
208
154
|
base_fields + associations + atf + asf
|
|
209
155
|
end
|
|
210
156
|
|
|
211
|
-
# Get the
|
|
212
|
-
def self.
|
|
157
|
+
# Get the association's fields that may be serialized and filtered/ordered for a reflection.
|
|
158
|
+
def self.association_fields_for(ref)
|
|
213
159
|
if !ref.polymorphic? && model = ref.klass
|
|
214
|
-
|
|
160
|
+
fields = [ model.primary_key ].flatten.compact
|
|
215
161
|
label_fields = RESTFramework.config.label_fields
|
|
216
162
|
|
|
217
163
|
# Preferably find a database column to use as label.
|
|
218
164
|
if match = label_fields.find { |f| f.in?(model.column_names) }
|
|
219
|
-
return
|
|
165
|
+
return fields + [ match ]
|
|
220
166
|
end
|
|
221
167
|
|
|
222
168
|
# Otherwise, find a method.
|
|
223
169
|
if match = label_fields.find { |f| model.method_defined?(f) }
|
|
224
|
-
return
|
|
170
|
+
return fields + [ match ]
|
|
225
171
|
end
|
|
226
172
|
|
|
227
|
-
return
|
|
173
|
+
return fields
|
|
228
174
|
end
|
|
229
175
|
|
|
230
176
|
[ "id", "name" ]
|
|
@@ -243,6 +189,30 @@ module RESTFramework::Utils
|
|
|
243
189
|
nil
|
|
244
190
|
end
|
|
245
191
|
|
|
192
|
+
# Find the REST controller for `model` at the same namespace level as `current_controller`, e.g.
|
|
193
|
+
# `Api::Demo::MoviesController` + `Genre` => `Api::Demo::GenresController`, or `nil` if none. The
|
|
194
|
+
# `model` match guards against trusting a same-named controller for a different model.
|
|
195
|
+
def self.controller_for_model(current_controller, model)
|
|
196
|
+
return nil unless model && (base_name = current_controller.name)
|
|
197
|
+
|
|
198
|
+
namespace = base_name.deconstantize
|
|
199
|
+
model_name = model.model_name
|
|
200
|
+
|
|
201
|
+
# Plural for a collection controller, singular for a singular-resource one.
|
|
202
|
+
[ model_name.plural, model_name.singular ].each do |name|
|
|
203
|
+
candidate_name = "#{name.camelize}Controller"
|
|
204
|
+
candidate_name = "#{namespace}::#{candidate_name}" if namespace.present?
|
|
205
|
+
|
|
206
|
+
candidate = candidate_name.safe_constantize
|
|
207
|
+
next unless candidate.is_a?(Class) && candidate.include?(RESTFramework::Controller)
|
|
208
|
+
next unless candidate.model == model
|
|
209
|
+
|
|
210
|
+
return candidate
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
nil
|
|
214
|
+
end
|
|
215
|
+
|
|
246
216
|
# Wrap a serializer with an adapter if it is an ActiveModel::Serializer.
|
|
247
217
|
def self.wrap_ams(s)
|
|
248
218
|
if defined?(ActiveModel::Serializer) && (s < ActiveModel::Serializer)
|
|
@@ -251,12 +221,4 @@ module RESTFramework::Utils
|
|
|
251
221
|
|
|
252
222
|
s
|
|
253
223
|
end
|
|
254
|
-
|
|
255
|
-
# Used for deprecated mixins that rely on model being determined from the controller name.
|
|
256
|
-
def self.get_model(controller_class)
|
|
257
|
-
begin
|
|
258
|
-
controller_class.name.demodulize.chomp("Controller").singularize.constantize
|
|
259
|
-
rescue NameError
|
|
260
|
-
end
|
|
261
|
-
end
|
|
262
224
|
end
|