rest_framework 0.9.16 → 0.11.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 +3 -3
- data/VERSION +1 -1
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +6 -6
- data/app/views/rest_framework/routes_and_forms/_raw_form.html.erb +1 -1
- data/app/views/rest_framework/routes_and_forms/routes/_route.html.erb +1 -1
- data/lib/rest_framework/errors/{nil_passed_to_api_response_error.rb → nil_passed_to_render_api_error.rb} +3 -3
- data/lib/rest_framework/errors.rb +1 -1
- data/lib/rest_framework/filters/ordering_filter.rb +4 -7
- data/lib/rest_framework/filters/query_filter.rb +1 -4
- data/lib/rest_framework/filters/ransack_filter.rb +4 -4
- data/lib/rest_framework/filters/search_filter.rb +3 -6
- data/lib/rest_framework/mixins/base_controller_mixin.rb +93 -97
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +7 -7
- data/lib/rest_framework/mixins/model_controller_mixin.rb +256 -222
- data/lib/rest_framework/paginators/page_number_paginator.rb +12 -11
- data/lib/rest_framework/routers.rb +11 -1
- data/lib/rest_framework/serializers/native_serializer.rb +24 -14
- data/lib/rest_framework/utils.rb +51 -40
- metadata +3 -3
@@ -24,14 +24,13 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
24
24
|
fields: nil,
|
25
25
|
field_config: nil,
|
26
26
|
|
27
|
-
# Options for what should be included/excluded from default fields.
|
28
|
-
exclude_associations: false,
|
29
|
-
}
|
30
|
-
RRF_BASE_MODEL_INSTANCE_CONFIG = {
|
31
27
|
# Attributes for finding records.
|
32
28
|
find_by_fields: nil,
|
33
29
|
find_by_query_param: "find_by",
|
34
30
|
|
31
|
+
# Options for what should be included/excluded from default fields.
|
32
|
+
exclude_associations: false,
|
33
|
+
|
35
34
|
# Options for handling request body parameters.
|
36
35
|
allowed_parameters: nil,
|
37
36
|
filter_pk_from_request_body: true,
|
@@ -103,13 +102,20 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
103
102
|
# If fields is a hash, then parse it.
|
104
103
|
if input_fields.is_a?(Hash)
|
105
104
|
return RESTFramework::Utils.parse_fields_hash(
|
106
|
-
input_fields,
|
105
|
+
input_fields,
|
106
|
+
self.get_model,
|
107
|
+
exclude_associations: self.exclude_associations,
|
108
|
+
action_text: self.enable_action_text,
|
109
|
+
active_storage: self.enable_active_storage,
|
107
110
|
)
|
108
111
|
elsif !input_fields
|
109
112
|
# Otherwise, if fields is nil, then fallback to columns.
|
110
113
|
model = self.get_model
|
111
114
|
return model ? RESTFramework::Utils.fields_for(
|
112
|
-
model,
|
115
|
+
model,
|
116
|
+
exclude_associations: self.exclude_associations,
|
117
|
+
action_text: self.enable_action_text,
|
118
|
+
active_storage: self.enable_active_storage,
|
113
119
|
) : []
|
114
120
|
elsif input_fields
|
115
121
|
input_fields = input_fields.map(&:to_s)
|
@@ -118,113 +124,72 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
118
124
|
return input_fields
|
119
125
|
end
|
120
126
|
|
121
|
-
# Get a field
|
122
|
-
def
|
123
|
-
|
124
|
-
@_field_config_for ||= {}
|
125
|
-
return @_field_config_for[f] if @_field_config_for[f]
|
126
|
-
|
127
|
-
config = self.field_config&.dig(f) || {}
|
128
|
-
|
129
|
-
# Default sub-fields if field is an association.
|
130
|
-
if ref = self.get_model.reflections[f.to_s]
|
131
|
-
if ref.polymorphic?
|
132
|
-
columns = {}
|
133
|
-
else
|
134
|
-
model = ref.klass
|
135
|
-
columns = model.columns_hash
|
136
|
-
end
|
137
|
-
config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
|
138
|
-
config[:sub_fields] = config[:sub_fields].map(&:to_s)
|
139
|
-
|
140
|
-
# Serialize very basic metadata about sub-fields.
|
141
|
-
config[:sub_fields_metadata] = config[:sub_fields].map { |sf|
|
142
|
-
v = {}
|
143
|
-
|
144
|
-
if columns[sf]
|
145
|
-
v[:kind] = "column"
|
146
|
-
end
|
147
|
-
|
148
|
-
next [sf, v]
|
149
|
-
}.to_h.compact.presence
|
150
|
-
end
|
151
|
-
|
152
|
-
return @_field_config_for[f] = config.compact
|
153
|
-
end
|
154
|
-
|
155
|
-
# Get metadata about the resource's fields.
|
156
|
-
def fields_metadata
|
157
|
-
return @_fields_metadata if @_fields_metadata
|
127
|
+
# Get a full field configuration, including defaults and inferred values.
|
128
|
+
def field_configuration
|
129
|
+
return @field_configuration if @field_configuration
|
158
130
|
|
159
|
-
|
131
|
+
field_config = self.field_config&.with_indifferent_access || {}
|
160
132
|
model = self.get_model
|
161
|
-
fields = self.get_fields.map(&:to_s)
|
162
133
|
columns = model.columns_hash
|
163
134
|
column_defaults = model.column_defaults
|
164
135
|
reflections = model.reflections
|
165
136
|
attributes = model._default_attributes
|
166
137
|
readonly_attributes = model.readonly_attributes
|
167
|
-
exclude_body_fields = self.exclude_body_fields
|
138
|
+
exclude_body_fields = self.exclude_body_fields&.map(&:to_s)
|
168
139
|
rich_text_association_names = model.reflect_on_all_associations(:has_one)
|
169
140
|
.collect(&:name)
|
170
141
|
.select { |n| n.to_s.start_with?("rich_text_") }
|
171
142
|
attachment_reflections = model.attachment_reflections
|
172
143
|
|
173
|
-
return @
|
174
|
-
|
175
|
-
|
176
|
-
type: nil,
|
177
|
-
kind: nil,
|
178
|
-
label: self.label_for(f),
|
179
|
-
primary_key: nil,
|
180
|
-
required: nil,
|
181
|
-
read_only: nil,
|
182
|
-
}
|
144
|
+
return @field_configuration = self.get_fields.map { |f|
|
145
|
+
cfg = field_config[f]&.dup || {}
|
146
|
+
cfg[:label] ||= self.label_for(f)
|
183
147
|
|
184
|
-
#
|
148
|
+
# Annotate primary key.
|
185
149
|
if model.primary_key == f
|
186
|
-
|
150
|
+
cfg[:primary_key] = true
|
151
|
+
|
152
|
+
unless cfg.key?(:readonly)
|
153
|
+
cfg[:readonly] = true
|
154
|
+
end
|
187
155
|
end
|
188
156
|
|
189
|
-
#
|
190
|
-
if
|
191
|
-
|
157
|
+
# Annotate readonly attributes.
|
158
|
+
if f.in?(readonly_attributes) || f.in?(exclude_body_fields)
|
159
|
+
cfg[:readonly] = true
|
192
160
|
end
|
193
161
|
|
194
|
-
#
|
162
|
+
# Annotate column data.
|
195
163
|
if column = columns[f]
|
196
|
-
|
197
|
-
|
198
|
-
|
164
|
+
cfg[:kind] = "column"
|
165
|
+
cfg[:type] ||= column.type
|
166
|
+
cfg[:required] = true unless column.null
|
199
167
|
end
|
200
168
|
|
201
|
-
#
|
202
|
-
|
203
|
-
|
204
|
-
unless column_default.nil?
|
205
|
-
metadata[:default] = column_default
|
169
|
+
# Add default values from the model's schema.
|
170
|
+
if column_default = column_defaults[f] && !cfg[:default].nil?
|
171
|
+
cfg[:default] ||= column_default
|
206
172
|
end
|
207
173
|
|
208
|
-
#
|
209
|
-
if
|
210
|
-
|
211
|
-
default =
|
212
|
-
metadata[:default] = default unless default.nil?
|
174
|
+
# Add metadata from the model's attributes hash.
|
175
|
+
if attribute = attributes[f]
|
176
|
+
if cfg[:default].nil? && default = attribute.value_before_type_cast
|
177
|
+
cfg[:default] = default
|
213
178
|
end
|
214
|
-
|
179
|
+
cfg[:kind] ||= "attribute"
|
215
180
|
|
216
181
|
# Get any type information from the attribute.
|
217
182
|
if type = attribute.type
|
218
|
-
|
183
|
+
cfg[:type] ||= type.type if type.type
|
219
184
|
|
220
185
|
# Get enum variants.
|
221
186
|
if type.is_a?(ActiveRecord::Enum::EnumType)
|
222
|
-
|
187
|
+
cfg[:enum_variants] = type.send(:mapping)
|
223
188
|
|
224
|
-
#
|
189
|
+
# TranslateEnum Integration:
|
225
190
|
translate_method = "translated_#{f.pluralize}"
|
226
191
|
if model.respond_to?(translate_method)
|
227
|
-
|
192
|
+
cfg[:enum_translations] = model.send(translate_method)
|
228
193
|
end
|
229
194
|
end
|
230
195
|
end
|
@@ -232,53 +197,65 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
232
197
|
|
233
198
|
# Get association metadata.
|
234
199
|
if ref = reflections[f]
|
235
|
-
|
200
|
+
cfg[:kind] = "association"
|
201
|
+
|
202
|
+
# Determine sub-fields for associations.
|
203
|
+
if ref.polymorphic?
|
204
|
+
ref_columns = {}
|
205
|
+
else
|
206
|
+
ref_columns = ref.klass.columns_hash
|
207
|
+
end
|
208
|
+
cfg[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
|
209
|
+
cfg[:sub_fields] = cfg[:sub_fields].map(&:to_s)
|
210
|
+
|
211
|
+
# Very basic metadata about sub-fields.
|
212
|
+
cfg[:sub_fields_metadata] = cfg[:sub_fields].map { |sf|
|
213
|
+
v = {}
|
214
|
+
|
215
|
+
if ref_columns[sf]
|
216
|
+
v[:kind] = "column"
|
217
|
+
else
|
218
|
+
v[:kind] = "method"
|
219
|
+
end
|
220
|
+
|
221
|
+
next [sf, v]
|
222
|
+
}.to_h.compact.presence
|
236
223
|
|
237
224
|
# Determine if we render id/ids fields. Unfortunately, `has_one` does not provide this
|
238
225
|
# interface.
|
239
|
-
if self.permit_id_assignment && id_field = RESTFramework::Utils.
|
240
|
-
|
226
|
+
if self.permit_id_assignment && id_field = RESTFramework::Utils.id_field_for(f, ref)
|
227
|
+
cfg[:id_field] = id_field
|
241
228
|
end
|
242
229
|
|
243
230
|
# Determine if we render nested attributes options.
|
244
231
|
if self.permit_nested_attributes_assignment && (
|
245
232
|
nested_opts = model.nested_attributes_options[f.to_sym].presence
|
246
233
|
)
|
247
|
-
|
234
|
+
cfg[:nested_attributes_options] = {field: "#{f}_attributes", **nested_opts}
|
248
235
|
end
|
249
236
|
|
250
237
|
begin
|
251
|
-
|
238
|
+
cfg[:association_pk] = ref.active_record_primary_key
|
252
239
|
rescue ActiveRecord::UnknownPrimaryKey
|
253
240
|
end
|
254
|
-
|
255
|
-
|
256
|
-
collection: ref.collection?,
|
257
|
-
class_name: ref.class_name,
|
258
|
-
foreign_key: ref.foreign_key,
|
259
|
-
primary_key: pk,
|
260
|
-
polymorphic: ref.polymorphic?,
|
261
|
-
table_name: ref.polymorphic? ? nil : ref.table_name,
|
262
|
-
options: ref.options.as_json.presence,
|
263
|
-
}.compact
|
241
|
+
|
242
|
+
cfg[:reflection] = ref
|
264
243
|
end
|
265
244
|
|
266
245
|
# Determine if this is an ActionText "rich text".
|
267
246
|
if :"rich_text_#{f}".in?(rich_text_association_names)
|
268
|
-
|
247
|
+
cfg[:kind] = "rich_text"
|
269
248
|
end
|
270
249
|
|
271
250
|
# Determine if this is an ActiveStorage attachment.
|
272
251
|
if ref = attachment_reflections[f]
|
273
|
-
|
274
|
-
|
275
|
-
macro: ref.macro,
|
276
|
-
}
|
252
|
+
cfg[:kind] = "attachment"
|
253
|
+
cfg[:attachment_type] = ref.macro
|
277
254
|
end
|
278
255
|
|
279
256
|
# Determine if this is just a method.
|
280
|
-
if !
|
281
|
-
|
257
|
+
if !cfg[:kind] && model.method_defined?(f)
|
258
|
+
cfg[:kind] = "method"
|
282
259
|
end
|
283
260
|
|
284
261
|
# Collect validator options into a hash on their type, while also updating `required` based
|
@@ -292,7 +269,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
292
269
|
next if IGNORE_VALIDATORS_WITH_KEYS.any? { |k| options.key?(k) }
|
293
270
|
|
294
271
|
# Update `required` if we find a presence validator.
|
295
|
-
|
272
|
+
cfg[:required] = true if kind == :presence
|
296
273
|
|
297
274
|
# Resolve procs (and lambdas), and symbols for certain arguments.
|
298
275
|
if options[:in].is_a?(Proc)
|
@@ -301,27 +278,65 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
301
278
|
options = options.merge(in: model.send(options[:in]))
|
302
279
|
end
|
303
280
|
|
304
|
-
|
305
|
-
|
306
|
-
|
281
|
+
cfg[:validators] ||= {}
|
282
|
+
cfg[:validators][kind] ||= []
|
283
|
+
cfg[:validators][kind] << options
|
307
284
|
end
|
308
285
|
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
next [f, metadata.compact]
|
313
|
-
}.to_h
|
286
|
+
next [f, cfg]
|
287
|
+
}.to_h.with_indifferent_access
|
314
288
|
end
|
315
289
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
290
|
+
def openapi_schema
|
291
|
+
return @openapi_schema if @openapi_schema
|
292
|
+
|
293
|
+
field_configuration = self.field_configuration
|
294
|
+
@openapi_schema = {
|
295
|
+
required: field_configuration.select { |_, cfg| cfg[:required] }.keys,
|
296
|
+
type: "object",
|
297
|
+
properties: field_configuration.map { |f, cfg|
|
298
|
+
v = {title: cfg[:label]}
|
299
|
+
|
300
|
+
if cfg[:kind] == "association"
|
301
|
+
v[:type] = cfg[:reflection].collection? ? "array" : "object"
|
302
|
+
elsif cfg[:kind] == "rich_text"
|
303
|
+
v[:type] = "string"
|
304
|
+
v[:"x-rrf-rich_text"] = true
|
305
|
+
elsif cfg[:kind] == "attachment"
|
306
|
+
v[:type] = "string"
|
307
|
+
v[:"x-rrf-attachment"] = cfg[:attachment_type]
|
308
|
+
else
|
309
|
+
v[:type] = cfg[:type]
|
310
|
+
end
|
311
|
+
|
312
|
+
v[:readOnly] = true if cfg[:readonly]
|
313
|
+
v[:default] = cfg[:default] if cfg.key?(:default)
|
314
|
+
|
315
|
+
if enum_variants = cfg[:enum_variants]
|
316
|
+
v[:enum] = enum_variants.keys
|
317
|
+
v[:"x-rrf-enum_variants"] = enum_variants
|
318
|
+
end
|
319
|
+
|
320
|
+
if validators = cfg[:validators]
|
321
|
+
v[:"x-rrf-validators"] = validators
|
322
|
+
end
|
323
|
+
|
324
|
+
v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
|
325
|
+
|
326
|
+
if cfg[:reflection]
|
327
|
+
v[:"x-rrf-reflection"] = cfg[:reflection]
|
328
|
+
v[:"x-rrf-association_pk"] = cfg[:association_pk]
|
329
|
+
v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
|
330
|
+
v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
|
331
|
+
v[:"x-rrf-id_field"] = cfg[:id_field]
|
332
|
+
v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
|
333
|
+
end
|
334
|
+
|
335
|
+
next [f, v]
|
336
|
+
}.to_h,
|
337
|
+
}
|
338
|
+
|
339
|
+
return @openapi_schema
|
325
340
|
end
|
326
341
|
|
327
342
|
def setup_delegation
|
@@ -334,9 +349,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
334
349
|
model = self.class.get_model
|
335
350
|
|
336
351
|
if model.method(action).parameters.last&.first == :keyrest
|
337
|
-
return
|
352
|
+
return render_api(model.send(action, **params))
|
338
353
|
else
|
339
|
-
return
|
354
|
+
return render_api(model.send(action))
|
340
355
|
end
|
341
356
|
end
|
342
357
|
end
|
@@ -350,9 +365,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
350
365
|
record = self.get_record
|
351
366
|
|
352
367
|
if record.method(action).parameters.last&.first == :keyrest
|
353
|
-
return
|
368
|
+
return render_api(record.send(action, **params))
|
354
369
|
else
|
355
|
-
return
|
370
|
+
return render_api(record.send(action))
|
356
371
|
end
|
357
372
|
end
|
358
373
|
end
|
@@ -366,7 +381,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
366
381
|
# self.setup_channel
|
367
382
|
|
368
383
|
if RESTFramework.config.freeze_config
|
369
|
-
|
384
|
+
self::RRF_BASE_MODEL_CONFIG.keys.each { |k|
|
370
385
|
v = self.send(k)
|
371
386
|
v.freeze if v.is_a?(Hash) || v.is_a?(Array)
|
372
387
|
}
|
@@ -388,27 +403,51 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
388
403
|
|
389
404
|
base.class_attribute(a, default: default, instance_accessor: false)
|
390
405
|
end
|
391
|
-
RRF_BASE_MODEL_INSTANCE_CONFIG.each do |a, default|
|
392
|
-
next if base.respond_to?(a)
|
393
|
-
|
394
|
-
base.class_attribute(a, default: default)
|
395
|
-
end
|
396
406
|
end
|
397
407
|
|
398
|
-
# Get a list of fields for this controller.
|
399
408
|
def get_fields
|
400
409
|
return self.class.get_fields(input_fields: self.class.fields)
|
401
410
|
end
|
402
411
|
|
403
|
-
def
|
404
|
-
|
412
|
+
def openapi_metadata
|
413
|
+
data = super
|
414
|
+
routes = self.route_groups.values[0]
|
415
|
+
schema_name = routes[0][:controller].camelize.gsub("::", ".")
|
416
|
+
|
417
|
+
# Insert schema into metadata.
|
418
|
+
data[:components] ||= {}
|
419
|
+
data[:components][:schemas] ||= {}
|
420
|
+
data[:components][:schemas][schema_name] = self.class.openapi_schema
|
421
|
+
|
422
|
+
# Reference schema for specific actions with a `requestBody`.
|
423
|
+
data[:paths].each do |_path, actions|
|
424
|
+
actions.each do |_method, action|
|
425
|
+
next unless action.is_a?(Hash)
|
426
|
+
|
427
|
+
injectables = [action.dig(:requestBody, :content), *action[:responses].values.map { |r|
|
428
|
+
r[:content]
|
429
|
+
}].compact
|
430
|
+
injectables.each do |i|
|
431
|
+
i.each do |_, v|
|
432
|
+
v[:schema] = {"$ref" => "#/components/schemas/#{schema_name}"}
|
433
|
+
end
|
434
|
+
end
|
435
|
+
end
|
436
|
+
end
|
437
|
+
|
438
|
+
return data.merge(
|
439
|
+
{
|
440
|
+
"x-rrf-primary_key" => self.class.get_model.primary_key,
|
441
|
+
"x-rrf-callbacks" => self._process_action_callbacks.as_json,
|
442
|
+
},
|
443
|
+
)
|
405
444
|
end
|
406
445
|
|
407
|
-
# Get a
|
446
|
+
# Get a hash of strong parameters for the current action.
|
408
447
|
def get_allowed_parameters
|
409
448
|
return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
|
410
449
|
|
411
|
-
@_get_allowed_parameters = self.allowed_parameters
|
450
|
+
@_get_allowed_parameters = self.class.allowed_parameters
|
412
451
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
413
452
|
|
414
453
|
# Assemble strong parameters.
|
@@ -417,46 +456,49 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
417
456
|
reflections = self.class.get_model.reflections
|
418
457
|
@_get_allowed_parameters = self.get_fields.map { |f|
|
419
458
|
f = f.to_s
|
459
|
+
config = self.class.field_configuration[f]
|
420
460
|
|
421
|
-
#
|
422
|
-
if reflections.key?("#{f}
|
423
|
-
hash_variations[f] = ACTIVESTORAGE_KEYS
|
461
|
+
# ActionText Integration:
|
462
|
+
if self.class.enable_action_text && reflections.key?("rich_test_#{f}")
|
424
463
|
next f
|
425
464
|
end
|
426
465
|
|
427
|
-
# ActiveStorage Integration: `
|
428
|
-
if reflections.key?("#{f}
|
466
|
+
# ActiveStorage Integration: `has_one_attached`
|
467
|
+
if self.class.enable_active_storage && reflections.key?("#{f}_attachment")
|
429
468
|
hash_variations[f] = ACTIVESTORAGE_KEYS
|
430
|
-
next
|
469
|
+
next f
|
431
470
|
end
|
432
471
|
|
433
|
-
#
|
434
|
-
if reflections.key?("
|
435
|
-
|
472
|
+
# ActiveStorage Integration: `has_many_attached`
|
473
|
+
if self.class.enable_active_storage && reflections.key?("#{f}_attachments")
|
474
|
+
hash_variations[f] = ACTIVESTORAGE_KEYS
|
475
|
+
next nil
|
436
476
|
end
|
437
477
|
|
438
|
-
|
439
|
-
|
478
|
+
if config[:reflection]
|
479
|
+
# Add `_id`/`_ids` variations for associations.
|
480
|
+
if id_field = config[:id_field]
|
481
|
+
if id_field.ends_with?("_ids")
|
482
|
+
hash_variations[id_field] = []
|
483
|
+
else
|
484
|
+
variations << id_field
|
485
|
+
end
|
486
|
+
end
|
440
487
|
|
441
|
-
|
442
|
-
|
443
|
-
if
|
444
|
-
hash_variations[
|
445
|
-
|
446
|
-
|
488
|
+
# Add `_attributes` variations for associations.
|
489
|
+
# TODO: Consider adjusting this based on `nested_attributes_options`.
|
490
|
+
if self.class.permit_nested_attributes_assignment
|
491
|
+
hash_variations["#{f}_attributes"] = (
|
492
|
+
config[:sub_fields] + ["_destroy"]
|
493
|
+
)
|
447
494
|
end
|
448
|
-
end
|
449
495
|
|
450
|
-
|
451
|
-
|
452
|
-
|
453
|
-
self.class.field_config_for(f)[:sub_fields] + ["_destroy"]
|
454
|
-
)
|
496
|
+
# Associations are not allowed to be submitted in their bare form (if they are submitted
|
497
|
+
# that way, they will be translated to either id/ids or nested attributes assignment).
|
498
|
+
next nil
|
455
499
|
end
|
456
500
|
|
457
|
-
|
458
|
-
# way, they will be translated to either ID assignment or nested attributes assignment).
|
459
|
-
next nil
|
501
|
+
next f
|
460
502
|
}.compact
|
461
503
|
@_get_allowed_parameters += variations
|
462
504
|
@_get_allowed_parameters << hash_variations
|
@@ -464,22 +506,11 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
464
506
|
return @_get_allowed_parameters
|
465
507
|
end
|
466
508
|
|
467
|
-
def
|
509
|
+
def get_serializer_class
|
468
510
|
return super || RESTFramework::NativeSerializer
|
469
511
|
end
|
470
512
|
|
471
|
-
|
472
|
-
# TODO: Compatibility; remove in 1.0.
|
473
|
-
if filtered_data = self.try(:get_filtered_data, data)
|
474
|
-
return filtered_data
|
475
|
-
end
|
476
|
-
|
477
|
-
return self.filter_backends&.reduce(data) { |d, filter|
|
478
|
-
filter.new(controller: self).filter_data(d)
|
479
|
-
} || data
|
480
|
-
end
|
481
|
-
|
482
|
-
# Use strong parameters to filter the request body using the configured allowed parameters.
|
513
|
+
# Use strong parameters to filter the request body.
|
483
514
|
def get_body_params(bulk_mode: nil)
|
484
515
|
data = self.request.request_parameters
|
485
516
|
pk = self.class.get_model&.primary_key
|
@@ -495,7 +526,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
495
526
|
# Assume nested attributes assignment.
|
496
527
|
attributes_key = "#{name}_attributes"
|
497
528
|
data[attributes_key] = data.delete(name) unless data[attributes_key]
|
498
|
-
elsif id_field = RESTFramework::Utils.
|
529
|
+
elsif id_field = RESTFramework::Utils.id_field_for(name, ref)
|
499
530
|
# Assume id/ids assignment.
|
500
531
|
data[id_field] = data.delete(name) unless data[id_field]
|
501
532
|
end
|
@@ -513,31 +544,33 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
513
544
|
#
|
514
545
|
# rubocop:enable Layout/LineLength
|
515
546
|
has_many_attached_scalar_data = {}
|
516
|
-
self.class.
|
517
|
-
|
518
|
-
|
519
|
-
|
520
|
-
v = BASE64_TRANSLATE.call(k, v)
|
521
|
-
|
522
|
-
# Remember scalars because Rails strong params will remove it.
|
547
|
+
if self.class.enable_active_storage
|
548
|
+
self.class.get_model.attachment_reflections.keys.each do |k|
|
549
|
+
if data[k].is_a?(Array)
|
550
|
+
data[k] = data[k].map { |v|
|
523
551
|
if v.is_a?(String)
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
552
|
+
v = BASE64_TRANSLATE.call(k, v)
|
553
|
+
|
554
|
+
# Remember scalars because Rails strong params will remove it.
|
555
|
+
if v.is_a?(String)
|
556
|
+
has_many_attached_scalar_data[k] ||= []
|
557
|
+
has_many_attached_scalar_data[k] << v
|
558
|
+
end
|
559
|
+
elsif v.is_a?(Hash)
|
560
|
+
if v[:io].is_a?(String)
|
561
|
+
v[:io] = StringIO.new(Base64.decode64(v[:io]))
|
562
|
+
end
|
530
563
|
end
|
531
|
-
end
|
532
564
|
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
565
|
+
next v
|
566
|
+
}
|
567
|
+
elsif data[k].is_a?(Hash)
|
568
|
+
if data[k][:io].is_a?(String)
|
569
|
+
data[k][:io] = StringIO.new(Base64.decode64(data[k][:io]))
|
570
|
+
end
|
571
|
+
elsif data[k].is_a?(String)
|
572
|
+
data[k] = BASE64_TRANSLATE.call(k, data[k])
|
538
573
|
end
|
539
|
-
elsif data[k].is_a?(String)
|
540
|
-
data[k] = BASE64_TRANSLATE.call(k, data[k])
|
541
574
|
end
|
542
575
|
end
|
543
576
|
|
@@ -562,12 +595,12 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
562
595
|
end
|
563
596
|
|
564
597
|
# Filter primary key, if configured.
|
565
|
-
if self.filter_pk_from_request_body && bulk_mode != :update
|
598
|
+
if self.class.filter_pk_from_request_body && bulk_mode != :update
|
566
599
|
body_params.delete(pk)
|
567
600
|
end
|
568
601
|
|
569
602
|
# Filter fields in `exclude_body_fields`.
|
570
|
-
(self.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
603
|
+
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
571
604
|
|
572
605
|
return body_params
|
573
606
|
end
|
@@ -586,13 +619,16 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
586
619
|
return nil
|
587
620
|
end
|
588
621
|
|
589
|
-
#
|
622
|
+
# Filter the recordset and return records this request has access to.
|
590
623
|
def get_records
|
591
|
-
|
624
|
+
data = self.get_recordset
|
625
|
+
|
626
|
+
return @records ||= self.class.filter_backends&.reduce(data) { |d, filter|
|
627
|
+
filter.new(controller: self).filter_data(d)
|
628
|
+
} || data
|
592
629
|
end
|
593
630
|
|
594
|
-
# Get a single record by primary key or another column, if allowed.
|
595
|
-
# and exposed to the view as the `@record` instance variable.
|
631
|
+
# Get a single record by primary key or another column, if allowed.
|
596
632
|
def get_record
|
597
633
|
return @record if @record
|
598
634
|
|
@@ -600,9 +636,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
600
636
|
is_pk = true
|
601
637
|
|
602
638
|
# Find by another column if it's permitted.
|
603
|
-
if find_by_param = self.find_by_query_param.presence
|
639
|
+
if find_by_param = self.class.find_by_query_param.presence
|
604
640
|
if find_by = params[find_by_param].presence
|
605
|
-
find_by_fields = self.find_by_fields&.map(&:to_s)
|
641
|
+
find_by_fields = self.class.find_by_fields&.map(&:to_s)
|
606
642
|
|
607
643
|
if !find_by_fields || find_by.in?(find_by_fields)
|
608
644
|
is_pk = false unless find_by_key == find_by
|
@@ -612,7 +648,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
612
648
|
end
|
613
649
|
|
614
650
|
# Get the recordset, filtering if configured.
|
615
|
-
collection = if self.filter_recordset_before_find
|
651
|
+
collection = if self.class.filter_recordset_before_find
|
616
652
|
self.get_records
|
617
653
|
else
|
618
654
|
self.get_recordset
|
@@ -628,7 +664,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
628
664
|
|
629
665
|
# Determine what collection to call `create` on.
|
630
666
|
def get_create_from
|
631
|
-
if self.create_from_recordset
|
667
|
+
if self.class.create_from_recordset
|
632
668
|
# Create with any properties inherited from the recordset. We exclude any `select` clauses
|
633
669
|
# in case model callbacks need to call `count` on this collection, which typically raises a
|
634
670
|
# SQL `SyntaxError`.
|
@@ -645,19 +681,17 @@ module RESTFramework::Mixins::BaseModelControllerMixin
|
|
645
681
|
# This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
|
646
682
|
# the serializer directly. This would fail for active model serializers, but maybe we don't
|
647
683
|
# care?
|
648
|
-
s = RESTFramework::Utils.wrap_ams(self.
|
649
|
-
|
684
|
+
s = RESTFramework::Utils.wrap_ams(self.get_serializer_class)
|
685
|
+
return records.map do |record|
|
650
686
|
s.new(record, controller: self).serialize.merge!({errors: record.errors.presence}.compact)
|
651
687
|
end
|
652
|
-
|
653
|
-
return serialized_records
|
654
688
|
end
|
655
689
|
end
|
656
690
|
|
657
691
|
# Mixin for listing records.
|
658
692
|
module RESTFramework::Mixins::ListModelMixin
|
659
693
|
def index
|
660
|
-
return
|
694
|
+
return render_api(self.get_index_records)
|
661
695
|
end
|
662
696
|
|
663
697
|
# Get records with both filtering and pagination applied.
|
@@ -665,13 +699,13 @@ module RESTFramework::Mixins::ListModelMixin
|
|
665
699
|
records = self.get_records
|
666
700
|
|
667
701
|
# Handle pagination, if enabled.
|
668
|
-
if self.paginator_class
|
669
|
-
#
|
670
|
-
# set to "0"
|
671
|
-
|
672
|
-
|
673
|
-
|
674
|
-
paginator =
|
702
|
+
if paginator_class = self.class.paginator_class
|
703
|
+
# Paginate if there is a `max_page_size`, or if there is no `page_size_query_param`, or if the
|
704
|
+
# page size is not set to "0".
|
705
|
+
max_page_size = self.class.max_page_size
|
706
|
+
page_size_query_param = self.class.page_size_query_param
|
707
|
+
if max_page_size || !page_size_query_param || params[page_size_query_param] != "0"
|
708
|
+
paginator = paginator_class.new(data: records, controller: self)
|
675
709
|
page = paginator.get_page
|
676
710
|
serialized_page = self.serialize(page)
|
677
711
|
return paginator.get_paginated_response(serialized_page)
|
@@ -685,14 +719,14 @@ end
|
|
685
719
|
# Mixin for showing records.
|
686
720
|
module RESTFramework::Mixins::ShowModelMixin
|
687
721
|
def show
|
688
|
-
return
|
722
|
+
return render_api(self.get_record)
|
689
723
|
end
|
690
724
|
end
|
691
725
|
|
692
726
|
# Mixin for creating records.
|
693
727
|
module RESTFramework::Mixins::CreateModelMixin
|
694
728
|
def create
|
695
|
-
return
|
729
|
+
return render_api(self.create!, status: :created)
|
696
730
|
end
|
697
731
|
|
698
732
|
# Perform the `create!` call and return the created record.
|
@@ -704,7 +738,7 @@ end
|
|
704
738
|
# Mixin for updating records.
|
705
739
|
module RESTFramework::Mixins::UpdateModelMixin
|
706
740
|
def update
|
707
|
-
return
|
741
|
+
return render_api(self.update!)
|
708
742
|
end
|
709
743
|
|
710
744
|
# Perform the `update!` call and return the updated record.
|
@@ -719,7 +753,7 @@ end
|
|
719
753
|
module RESTFramework::Mixins::DestroyModelMixin
|
720
754
|
def destroy
|
721
755
|
self.destroy!
|
722
|
-
return
|
756
|
+
return render_api("")
|
723
757
|
end
|
724
758
|
|
725
759
|
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|