rest_framework 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +31 -26
- data/VERSION +1 -1
- data/app/views/rest_framework/_head.html.erb +1 -6
- data/app/views/rest_framework/_routes_and_forms.html.erb +2 -5
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/lib/rest_framework/controller/bulk.rb +62 -0
- data/lib/rest_framework/controller/crud.rb +66 -0
- data/lib/rest_framework/controller/openapi.rb +249 -0
- data/lib/rest_framework/controller.rb +804 -0
- data/lib/rest_framework/engine.rb +12 -2
- data/lib/rest_framework/errors.rb +0 -1
- data/lib/rest_framework/filters/search_filter.rb +2 -2
- data/lib/rest_framework/mixins/base_controller_mixin.rb +3 -383
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +27 -68
- data/lib/rest_framework/mixins/model_controller_mixin.rb +60 -807
- data/lib/rest_framework/routers.rb +11 -6
- data/lib/rest_framework/serializers/native_serializer.rb +1 -1
- data/lib/rest_framework/utils.rb +17 -6
- data/lib/rest_framework.rb +12 -1
- metadata +6 -3
- data/lib/rest_framework/errors/unknown_model_error.rb +0 -18
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
class RESTFramework::Engine < Rails::Engine
|
|
2
|
-
initializer "rest_framework.assets" do
|
|
3
|
-
config.after_initialize do
|
|
2
|
+
initializer "rest_framework.assets" do |app|
|
|
3
|
+
app.config.after_initialize do
|
|
4
4
|
if RESTFramework.config.use_vendored_assets
|
|
5
5
|
app.config.assets.precompile += [
|
|
6
6
|
RESTFramework::EXTERNAL_CSS_NAME,
|
|
@@ -8,7 +8,11 @@ class RESTFramework::Engine < Rails::Engine
|
|
|
8
8
|
*RESTFramework::EXTERNAL_UNSUMMARIZED_ASSETS.keys.map { |name| "rest_framework/#{name}" },
|
|
9
9
|
]
|
|
10
10
|
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
11
13
|
|
|
14
|
+
initializer "rest_framework.register_api_renderer" do |app|
|
|
15
|
+
app.config.after_initialize do
|
|
12
16
|
if RESTFramework.config.register_api_renderer
|
|
13
17
|
ActionController::Renderers.add(:api) do |data, kwargs|
|
|
14
18
|
render_api(data, **kwargs)
|
|
@@ -16,4 +20,10 @@ class RESTFramework::Engine < Rails::Engine
|
|
|
16
20
|
end
|
|
17
21
|
end
|
|
18
22
|
end
|
|
23
|
+
|
|
24
|
+
initializer "rest_framework.deprecator" do |app|
|
|
25
|
+
if Rails::VERSION::MAJOR >= 8
|
|
26
|
+
app.deprecators[:rest_framework] = RESTFramework.deprecator
|
|
27
|
+
end
|
|
28
|
+
end
|
|
19
29
|
end
|
|
@@ -5,7 +5,7 @@ class RESTFramework::Filters::SearchFilter < RESTFramework::Filters::BaseFilter
|
|
|
5
5
|
return search_fields&.map(&:to_s)
|
|
6
6
|
end
|
|
7
7
|
|
|
8
|
-
columns = @controller.class.
|
|
8
|
+
columns = @controller.class.model.column_names
|
|
9
9
|
@controller.get_fields.select { |f|
|
|
10
10
|
f.in?(RESTFramework.config.search_columns) && f.in?(columns)
|
|
11
11
|
}
|
|
@@ -18,7 +18,7 @@ class RESTFramework::Filters::SearchFilter < RESTFramework::Filters::BaseFilter
|
|
|
18
18
|
if search.present?
|
|
19
19
|
if fields = self._get_fields.presence
|
|
20
20
|
# MySQL doesn't support casting to VARCHAR, so we need to use CHAR instead.
|
|
21
|
-
data_type = if data.connection.adapter_name =~ /mysql/i
|
|
21
|
+
data_type = if data.connection.adapter_name =~ /mysql|trilogy/i
|
|
22
22
|
"CHAR"
|
|
23
23
|
else
|
|
24
24
|
# Sufficient for both PostgreSQL and SQLite.
|
|
@@ -1,390 +1,10 @@
|
|
|
1
|
-
# This module provides the common functionality for any controller mixins, a `root` action, and
|
|
2
|
-
# the ability to route arbitrary actions with `extra_actions`. This is also where `render_api` is
|
|
3
|
-
# implemented.
|
|
4
1
|
module RESTFramework::Mixins::BaseControllerMixin
|
|
5
|
-
RRF_BASE_CONFIG = {
|
|
6
|
-
extra_actions: nil,
|
|
7
|
-
extra_member_actions: nil,
|
|
8
|
-
singleton_controller: nil,
|
|
9
|
-
|
|
10
|
-
# Options related to metadata and display.
|
|
11
|
-
title: nil,
|
|
12
|
-
description: nil,
|
|
13
|
-
version: nil,
|
|
14
|
-
inflect_acronyms: [ "ID", "IDs", "REST", "API", "APIs" ].freeze,
|
|
15
|
-
openapi_include_children: false,
|
|
16
|
-
|
|
17
|
-
# Options related to serialization.
|
|
18
|
-
rescue_unknown_format_with: :json,
|
|
19
|
-
serializer_class: nil,
|
|
20
|
-
serialize_to_json: true,
|
|
21
|
-
serialize_to_xml: true,
|
|
22
|
-
|
|
23
|
-
# Options related to pagination.
|
|
24
|
-
paginator_class: nil,
|
|
25
|
-
page_size: 20,
|
|
26
|
-
page_query_param: "page",
|
|
27
|
-
page_size_query_param: "page_size",
|
|
28
|
-
max_page_size: nil,
|
|
29
|
-
|
|
30
|
-
# Option to disable serializer adapters by default, mainly introduced because Active Model
|
|
31
|
-
# Serializers will do things like serialize `[]` into `{"":[]}`.
|
|
32
|
-
disable_adapters_by_default: true,
|
|
33
|
-
|
|
34
|
-
# Custom integrations (reduces serializer performance due to method calls).
|
|
35
|
-
enable_action_text: false,
|
|
36
|
-
enable_active_storage: false,
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
# Default action for API root.
|
|
40
|
-
def root
|
|
41
|
-
render(api: { message: "This is the API root." })
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
module ClassMethods
|
|
45
|
-
# By default, this is the name of the controller class, titleized and with any custom inflection
|
|
46
|
-
# acronyms applied.
|
|
47
|
-
def get_title
|
|
48
|
-
self.title || RESTFramework::Utils.inflect(
|
|
49
|
-
self.name.demodulize.chomp("Controller").titleize(keep_id_suffix: true),
|
|
50
|
-
self.inflect_acronyms,
|
|
51
|
-
)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Get a label from a field/column name, titleized and inflected.
|
|
55
|
-
def label_for(s)
|
|
56
|
-
RESTFramework::Utils.inflect(s.to_s.titleize(keep_id_suffix: true), self.inflect_acronyms)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Define any behavior to execute at the end of controller definition.
|
|
60
|
-
# :nocov:
|
|
61
|
-
def rrf_finalize
|
|
62
|
-
if RESTFramework.config.freeze_config
|
|
63
|
-
self::RRF_BASE_CONFIG.keys.each { |k|
|
|
64
|
-
v = self.send(k)
|
|
65
|
-
v.freeze if v.is_a?(Hash) || v.is_a?(Array)
|
|
66
|
-
}
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
# :nocov:
|
|
70
|
-
|
|
71
|
-
def openapi_response_content_types
|
|
72
|
-
@openapi_response_content_types ||= [
|
|
73
|
-
"text/html",
|
|
74
|
-
self.serialize_to_json ? "application/json" : nil,
|
|
75
|
-
self.serialize_to_xml ? "application/xml" : nil,
|
|
76
|
-
].compact
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def openapi_request_content_types
|
|
80
|
-
@openapi_request_content_types ||= [
|
|
81
|
-
"application/json",
|
|
82
|
-
"application/x-www-form-urlencoded",
|
|
83
|
-
"multipart/form-data",
|
|
84
|
-
]
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
def openapi_paths(routes, tag)
|
|
88
|
-
resp_cts = self.openapi_response_content_types
|
|
89
|
-
req_cts = self.openapi_request_content_types
|
|
90
|
-
|
|
91
|
-
routes.group_by { |r| r[:concat_path] }.map { |concat_path, routes|
|
|
92
|
-
[
|
|
93
|
-
concat_path.gsub(/:([0-9A-Za-z_-]+)/, "{\\1}"),
|
|
94
|
-
routes.map { |route|
|
|
95
|
-
metadata = RESTFramework::ROUTE_METADATA[route[:path]] || {}
|
|
96
|
-
summary = metadata.delete(:label).presence || self.label_for(route[:action])
|
|
97
|
-
description = metadata.delete(:description).presence
|
|
98
|
-
extra_action = RESTFramework::EXTRA_ACTION_ROUTES.include?(route[:path])
|
|
99
|
-
error_response = { "$ref" => "#/components/responses/BadRequest" }
|
|
100
|
-
not_found_response = { "$ref" => "#/components/responses/NotFound" }
|
|
101
|
-
spec = { tags: [ tag ], summary: summary, description: description }.compact
|
|
102
|
-
|
|
103
|
-
# All routes should have a successful response.
|
|
104
|
-
spec[:responses] = {
|
|
105
|
-
200 => { content: resp_cts.map { |ct| [ ct, {} ] }.to_h, description: "Success" },
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
# Builtin POST, PUT, PATCH, and DELETE should have a 400 and 404 response.
|
|
109
|
-
if route[:verb].in?([ "POST", "PUT", "PATCH", "DELETE" ]) && !extra_action
|
|
110
|
-
spec[:responses][400] = error_response
|
|
111
|
-
spec[:responses][404] = not_found_response
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# All POST, PUT, PATCH should have a request body.
|
|
115
|
-
if route[:verb].in?([ "POST", "PUT", "PATCH" ])
|
|
116
|
-
spec[:requestBody] ||= { content: req_cts.map { |ct| [ ct, {} ] }.to_h }
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
# Add remaining metadata as an extension.
|
|
120
|
-
spec["x-rrf-metadata"] = metadata if metadata.present?
|
|
121
|
-
|
|
122
|
-
next route[:verb].downcase, spec
|
|
123
|
-
}.to_h.merge(
|
|
124
|
-
{
|
|
125
|
-
parameters: routes.first[:route].required_parts.map { |p|
|
|
126
|
-
{
|
|
127
|
-
name: p,
|
|
128
|
-
in: "path",
|
|
129
|
-
required: true,
|
|
130
|
-
schema: { type: "integer" },
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
),
|
|
135
|
-
]
|
|
136
|
-
}.to_h
|
|
137
|
-
end
|
|
138
|
-
|
|
139
|
-
def openapi_document(request, route_group_name, routes)
|
|
140
|
-
server = request.base_url + request.original_fullpath.gsub(/\?.*/, "")
|
|
141
|
-
|
|
142
|
-
{
|
|
143
|
-
openapi: "3.1.1",
|
|
144
|
-
info: {
|
|
145
|
-
title: self.get_title,
|
|
146
|
-
description: self.description,
|
|
147
|
-
version: self.version.to_s,
|
|
148
|
-
}.compact,
|
|
149
|
-
servers: [ { url: server } ],
|
|
150
|
-
paths: self.openapi_paths(routes, route_group_name),
|
|
151
|
-
tags: [ { name: route_group_name, description: self.description }.compact ],
|
|
152
|
-
components: {
|
|
153
|
-
schemas: {
|
|
154
|
-
"Error" => {
|
|
155
|
-
type: "object",
|
|
156
|
-
required: [ "message" ],
|
|
157
|
-
properties: {
|
|
158
|
-
message: { type: "string" },
|
|
159
|
-
errors: { type: "object" },
|
|
160
|
-
exception: { type: "string" },
|
|
161
|
-
},
|
|
162
|
-
},
|
|
163
|
-
},
|
|
164
|
-
responses: {
|
|
165
|
-
"BadRequest": {
|
|
166
|
-
description: "Bad Request",
|
|
167
|
-
content: self.openapi_response_content_types.map { |ct|
|
|
168
|
-
[
|
|
169
|
-
ct,
|
|
170
|
-
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
|
171
|
-
]
|
|
172
|
-
}.to_h,
|
|
173
|
-
},
|
|
174
|
-
"NotFound": {
|
|
175
|
-
description: "Not Found",
|
|
176
|
-
content: self.openapi_response_content_types.map { |ct|
|
|
177
|
-
[
|
|
178
|
-
ct,
|
|
179
|
-
ct == "text/html" ? {} : { schema: { "$ref" => "#/components/schemas/Error" } },
|
|
180
|
-
]
|
|
181
|
-
}.to_h,
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
},
|
|
185
|
-
}.compact
|
|
186
|
-
end
|
|
187
|
-
end
|
|
188
|
-
|
|
189
2
|
def self.included(base)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
base.extend(ClassMethods)
|
|
193
|
-
|
|
194
|
-
# By default, the layout should be set to `rest_framework`.
|
|
195
|
-
base.layout("rest_framework")
|
|
196
|
-
|
|
197
|
-
# Add class attributes unless they already exist.
|
|
198
|
-
RRF_BASE_CONFIG.each do |a, default|
|
|
199
|
-
next if base.respond_to?(a)
|
|
200
|
-
|
|
201
|
-
# Don't leak class attributes to the instance to avoid conflicting with action methods.
|
|
202
|
-
base.class_attribute(a, default: default, instance_accessor: false)
|
|
203
|
-
end
|
|
204
|
-
|
|
205
|
-
# Alias `extra_actions` to `extra_collection_actions`.
|
|
206
|
-
unless base.respond_to?(:extra_collection_actions)
|
|
207
|
-
base.singleton_class.alias_method(:extra_collection_actions, :extra_actions)
|
|
208
|
-
base.singleton_class.alias_method(:extra_collection_actions=, :extra_actions=)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
# Skip CSRF since this is an API.
|
|
212
|
-
begin
|
|
213
|
-
base.skip_before_action(:verify_authenticity_token)
|
|
214
|
-
rescue
|
|
215
|
-
nil
|
|
216
|
-
end
|
|
217
|
-
|
|
218
|
-
# Handle some common exceptions.
|
|
219
|
-
unless RESTFramework.config.disable_rescue_from
|
|
220
|
-
base.rescue_from(
|
|
221
|
-
ActionController::ParameterMissing,
|
|
222
|
-
ActionController::UnpermittedParameters,
|
|
223
|
-
ActionDispatch::Http::Parameters::ParseError,
|
|
224
|
-
ActiveRecord::AssociationTypeMismatch,
|
|
225
|
-
ActiveRecord::NotNullViolation,
|
|
226
|
-
ActiveRecord::RecordNotFound,
|
|
227
|
-
ActiveRecord::RecordInvalid,
|
|
228
|
-
ActiveRecord::RecordNotSaved,
|
|
229
|
-
ActiveRecord::RecordNotDestroyed,
|
|
230
|
-
ActiveRecord::RecordNotUnique,
|
|
231
|
-
ActiveModel::UnknownAttributeError,
|
|
232
|
-
with: :rrf_error_handler,
|
|
233
|
-
)
|
|
234
|
-
end
|
|
235
|
-
|
|
236
|
-
# Use `TracePoint` hook to automatically call `rrf_finalize`.
|
|
237
|
-
if RESTFramework.config.auto_finalize
|
|
238
|
-
# :nocov:
|
|
239
|
-
TracePoint.trace(:end) do |t|
|
|
240
|
-
next if base != t.self
|
|
241
|
-
|
|
242
|
-
base.rrf_finalize
|
|
243
|
-
|
|
244
|
-
# It's important to disable the trace once we've found the end of the base class definition,
|
|
245
|
-
# for performance.
|
|
246
|
-
t.disable
|
|
247
|
-
end
|
|
248
|
-
# :nocov:
|
|
249
|
-
end
|
|
250
|
-
end
|
|
251
|
-
|
|
252
|
-
def get_serializer_class
|
|
253
|
-
self.class.serializer_class
|
|
254
|
-
end
|
|
255
|
-
|
|
256
|
-
# Serialize the given data using the `serializer_class`.
|
|
257
|
-
def serialize(data, **kwargs)
|
|
258
|
-
RESTFramework::Utils.wrap_ams(self.get_serializer_class).new(
|
|
259
|
-
data, controller: self, **kwargs
|
|
260
|
-
).serialize
|
|
261
|
-
end
|
|
262
|
-
|
|
263
|
-
def rrf_error_handler(e)
|
|
264
|
-
status = case e
|
|
265
|
-
when ActiveRecord::RecordNotFound
|
|
266
|
-
404
|
|
267
|
-
else
|
|
268
|
-
400
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
render(
|
|
272
|
-
api: {
|
|
273
|
-
message: e.message,
|
|
274
|
-
errors: e.try(:record).try(:errors),
|
|
275
|
-
exception: RESTFramework.config.show_backtrace ? e.full_message : nil,
|
|
276
|
-
}.compact,
|
|
277
|
-
status: status,
|
|
3
|
+
RESTFramework.deprecator.warn(
|
|
4
|
+
"BaseControllerMixin is deprecated; use RESTFramework::Controller instead.",
|
|
278
5
|
)
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
def route_groups
|
|
282
|
-
@route_groups ||= RESTFramework::Utils.get_routes(Rails.application.routes, request)
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
# Render a browsable API for `html` format, along with basic `json`/`xml` formats, and with
|
|
286
|
-
# support or passing custom `kwargs` to the underlying `render` calls.
|
|
287
|
-
def render_api(payload, **kwargs)
|
|
288
|
-
html_kwargs = kwargs.delete(:html_kwargs) || {}
|
|
289
|
-
json_kwargs = kwargs.delete(:json_kwargs) || {}
|
|
290
|
-
xml_kwargs = kwargs.delete(:xml_kwargs) || {}
|
|
291
|
-
|
|
292
|
-
# Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
|
|
293
|
-
# when passing something like `User.find_by(id: some_id)` to `render_api`). The caller should
|
|
294
|
-
# actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
|
|
295
|
-
# framework to catch this error and return an appropriate error response.
|
|
296
|
-
if payload.nil?
|
|
297
|
-
raise RESTFramework::NilPassedToRenderAPIError
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
# If `payload` is an `ActiveRecord::Relation` or `ActiveRecord::Base`, then serialize it.
|
|
301
|
-
if payload.is_a?(ActiveRecord::Base) || payload.is_a?(ActiveRecord::Relation)
|
|
302
|
-
payload = self.serialize(payload)
|
|
303
|
-
end
|
|
304
|
-
|
|
305
|
-
# Do not use any adapters by default, if configured.
|
|
306
|
-
if self.class.disable_adapters_by_default && !kwargs.key?(:adapter)
|
|
307
|
-
kwargs[:adapter] = nil
|
|
308
|
-
end
|
|
309
|
-
|
|
310
|
-
# Flag to track if we had to rescue unknown format.
|
|
311
|
-
already_rescued_unknown_format = false
|
|
312
|
-
|
|
313
|
-
begin
|
|
314
|
-
respond_to do |format|
|
|
315
|
-
if payload == ""
|
|
316
|
-
format.json { head(kwargs[:status] || :no_content) } if self.class.serialize_to_json
|
|
317
|
-
format.xml { head(kwargs[:status] || :no_content) } if self.class.serialize_to_xml
|
|
318
|
-
else
|
|
319
|
-
format.json {
|
|
320
|
-
render(json: payload, **kwargs.merge(json_kwargs))
|
|
321
|
-
} if self.class.serialize_to_json
|
|
322
|
-
format.xml {
|
|
323
|
-
render(xml: payload, **kwargs.merge(xml_kwargs))
|
|
324
|
-
} if self.class.serialize_to_xml
|
|
325
|
-
# TODO: possibly support more formats here if supported?
|
|
326
|
-
end
|
|
327
|
-
format.html {
|
|
328
|
-
@payload = payload
|
|
329
|
-
if payload == ""
|
|
330
|
-
@json_payload = "" if self.class.serialize_to_json
|
|
331
|
-
@xml_payload = "" if self.class.serialize_to_xml
|
|
332
|
-
else
|
|
333
|
-
@json_payload = payload.to_json if self.class.serialize_to_json
|
|
334
|
-
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
|
335
|
-
end
|
|
336
|
-
@title ||= self.class.get_title
|
|
337
|
-
@description ||= self.class.description
|
|
338
|
-
self.route_groups
|
|
339
|
-
begin
|
|
340
|
-
render(**kwargs.merge(html_kwargs))
|
|
341
|
-
rescue ActionView::MissingTemplate
|
|
342
|
-
# A view is not required, so just use `html: ""`.
|
|
343
|
-
render(html: "", layout: true, **kwargs.merge(html_kwargs))
|
|
344
|
-
end
|
|
345
|
-
}
|
|
346
|
-
end
|
|
347
|
-
rescue ActionController::UnknownFormat
|
|
348
|
-
if !already_rescued_unknown_format && rescue_format = self.class.rescue_unknown_format_with
|
|
349
|
-
request.format = rescue_format
|
|
350
|
-
already_rescued_unknown_format = true
|
|
351
|
-
retry
|
|
352
|
-
else
|
|
353
|
-
raise
|
|
354
|
-
end
|
|
355
|
-
end
|
|
356
|
-
end
|
|
357
|
-
|
|
358
|
-
# Compatibility alias for deprecated `api_response`.
|
|
359
|
-
alias_method :api_response, :render_api
|
|
360
|
-
|
|
361
|
-
def openapi_document
|
|
362
|
-
first, *rest = self.route_groups.to_a
|
|
363
|
-
document = self.class.openapi_document(request, *first)
|
|
364
|
-
|
|
365
|
-
if self.class.openapi_include_children
|
|
366
|
-
rest.each do |route_group_name, routes|
|
|
367
|
-
controller = "#{routes.first[:route].defaults[:controller]}_controller".camelize.constantize
|
|
368
|
-
child_document = controller.openapi_document(request, route_group_name, routes)
|
|
369
|
-
|
|
370
|
-
# Merge child paths and tags into the parent document.
|
|
371
|
-
document[:paths].merge!(child_document[:paths])
|
|
372
|
-
document[:tags] += child_document[:tags]
|
|
373
|
-
|
|
374
|
-
# If the child document has schemas, merge them into the parent document.
|
|
375
|
-
if schemas = child_document.dig(:components, :schemas) # rubocop:disable Style/Next
|
|
376
|
-
document[:components] ||= {}
|
|
377
|
-
document[:components][:schemas] ||= {}
|
|
378
|
-
document[:components][:schemas].merge!(schemas)
|
|
379
|
-
end
|
|
380
|
-
end
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
document
|
|
384
|
-
end
|
|
385
6
|
|
|
386
|
-
|
|
387
|
-
render(api: self.openapi_document)
|
|
7
|
+
base.include(RESTFramework::Controller)
|
|
388
8
|
end
|
|
389
9
|
end
|
|
390
10
|
|
|
@@ -1,91 +1,50 @@
|
|
|
1
|
-
require_relative "model_controller_mixin"
|
|
2
|
-
|
|
3
|
-
# Mixin for creating records in bulk. This is unique compared to update/destroy because we overload
|
|
4
|
-
# the existing `create` action to support bulk creation.
|
|
5
1
|
module RESTFramework::Mixins::BulkCreateModelMixin
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
super.merge({ "x-rrf-bulk-create": true })
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
def create
|
|
14
|
-
if params[:_json].is_a?(Array)
|
|
15
|
-
records = self.create_all!
|
|
16
|
-
serialized_records = self.bulk_serialize(records)
|
|
17
|
-
return render(api: serialized_records)
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
super
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Perform the `create` call, and return the collection of (possibly) created records.
|
|
24
|
-
def create_all!
|
|
25
|
-
create_data = self.get_create_params(bulk_mode: true)[:_json]
|
|
2
|
+
def self.included(base)
|
|
3
|
+
RESTFramework.deprecator.warn(<<~TXT).squish
|
|
4
|
+
BulkCreateModelMixin is deprecated; set the `bulk = true` class attribute instead.
|
|
5
|
+
TXT
|
|
26
6
|
|
|
27
|
-
|
|
28
|
-
ActiveRecord::Base.transaction { self.create_from.create(create_data) }
|
|
7
|
+
base.bulk = true
|
|
29
8
|
end
|
|
30
9
|
end
|
|
31
10
|
|
|
32
11
|
# Mixin for updating records in bulk.
|
|
33
12
|
module RESTFramework::Mixins::BulkUpdateModelMixin
|
|
34
|
-
def
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
# Perform the `update` call and return the collection of (possibly) updated records.
|
|
41
|
-
def update_all!
|
|
42
|
-
pk = self.class.get_model.primary_key
|
|
43
|
-
data = if params[:_json].is_a?(Array)
|
|
44
|
-
self.get_create_params(bulk_mode: :update)[:_json].index_by { |r| r[pk] }
|
|
45
|
-
else
|
|
46
|
-
create_params = self.get_create_params
|
|
47
|
-
{ create_params[pk] => create_params }
|
|
48
|
-
end
|
|
13
|
+
def self.included(base)
|
|
14
|
+
RESTFramework.deprecator.warn(<<~TXT).squish
|
|
15
|
+
BulkUpdateModelMixin is deprecated; set the `bulk = true`, and `excluded_actions` class
|
|
16
|
+
attributes instead.
|
|
17
|
+
TXT
|
|
49
18
|
|
|
50
|
-
|
|
51
|
-
|
|
19
|
+
base.bulk = true
|
|
20
|
+
base.excluded_actions = (base.excluded_actions - [ :update_all ]).freeze
|
|
52
21
|
end
|
|
53
22
|
end
|
|
54
23
|
|
|
55
24
|
# Mixin for destroying records in bulk.
|
|
56
25
|
module RESTFramework::Mixins::BulkDestroyModelMixin
|
|
57
|
-
def
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
render(
|
|
65
|
-
api: { message: "Bulk destroy requires an array of primary keys as input." }, status: 400,
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
|
70
|
-
def destroy_all!
|
|
71
|
-
pk = self.class.get_model.primary_key
|
|
72
|
-
destroy_data = self.request.request_parameters[:_json]
|
|
26
|
+
def self.included(base)
|
|
27
|
+
RESTFramework.deprecator.warn(<<~TXT).squish
|
|
28
|
+
BulkDestroyModelMixin is deprecated; set the `bulk = true`, and `excluded_actions` class
|
|
29
|
+
attributes instead.
|
|
30
|
+
TXT
|
|
73
31
|
|
|
74
|
-
|
|
75
|
-
|
|
32
|
+
base.bulk = true
|
|
33
|
+
base.excluded_actions = (base.excluded_actions - [ :destroy_all ]).freeze
|
|
76
34
|
end
|
|
77
35
|
end
|
|
78
36
|
|
|
79
37
|
# Mixin that includes all the CRUD bulk mixins.
|
|
80
38
|
module RESTFramework::Mixins::BulkModelControllerMixin
|
|
81
|
-
include RESTFramework::Mixins::ModelControllerMixin
|
|
82
|
-
|
|
83
|
-
include RESTFramework::Mixins::BulkCreateModelMixin
|
|
84
|
-
include RESTFramework::Mixins::BulkUpdateModelMixin
|
|
85
|
-
include RESTFramework::Mixins::BulkDestroyModelMixin
|
|
86
|
-
|
|
87
39
|
def self.included(base)
|
|
88
|
-
RESTFramework
|
|
40
|
+
RESTFramework.deprecator.warn(<<~TXT).squish
|
|
41
|
+
BulkModelControllerMixin is deprecated; use RESTFramework::Controller and set the `model` and
|
|
42
|
+
`bulk = true` class attributes instead.
|
|
43
|
+
TXT
|
|
44
|
+
|
|
45
|
+
base.include(RESTFramework::Controller)
|
|
46
|
+
base.model = RESTFramework::Utils.get_model(base)
|
|
47
|
+
base.bulk = true
|
|
89
48
|
end
|
|
90
49
|
end
|
|
91
50
|
|