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.
@@ -1,848 +1,101 @@
1
- # This module provides the core functionality for controllers based on models.
2
1
  module RESTFramework::Mixins::BaseModelControllerMixin
3
- BASE64_REGEX = /data:(.*);base64,(.*)/
4
- BASE64_TRANSLATE = ->(field, value) {
5
- return value unless BASE64_REGEX.match?(value)
6
-
7
- _, content_type, payload = value.match(BASE64_REGEX).to_a
8
- {
9
- io: StringIO.new(Base64.decode64(payload)),
10
- content_type: content_type,
11
- filename: "file_#{field}#{Rack::Mime::MIME_TYPES.invert[content_type]}",
12
- }
13
- }
14
- ACTIVESTORAGE_KEYS = [ :io, :content_type, :filename, :identify, :key ]
15
-
16
- include RESTFramework::BaseControllerMixin
17
-
18
- RRF_BASE_MODEL_CONFIG = {
19
- # Core attributes related to models.
20
- model: nil,
21
- recordset: nil,
22
-
23
- # Attributes for configuring record fields.
24
- fields: nil,
25
- field_config: nil,
26
- read_only_fields: RESTFramework.config.read_only_fields,
27
- write_only_fields: RESTFramework.config.write_only_fields,
28
- hidden_fields: nil,
29
-
30
- # Attributes for finding records.
31
- find_by_fields: nil,
32
- find_by_query_param: "find_by".freeze,
33
-
34
- # Options for what should be included/excluded from default fields.
35
- exclude_associations: false,
36
-
37
- # Options for handling request body parameters.
38
- allowed_parameters: nil,
39
-
40
- # Attributes for the default native serializer.
41
- native_serializer_config: nil,
42
- native_serializer_singular_config: nil,
43
- native_serializer_plural_config: nil,
44
- native_serializer_only_query_param: "only".freeze,
45
- native_serializer_except_query_param: "except".freeze,
46
- native_serializer_include_query_param: "include".freeze,
47
- native_serializer_exclude_query_param: "exclude".freeze,
48
- native_serializer_associations_limit: nil,
49
- native_serializer_associations_limit_query_param: "associations_limit".freeze,
50
- native_serializer_include_associations_count: false,
51
-
52
- # Attributes for filtering, ordering, and searching.
53
- filter_backends: [
54
- RESTFramework::QueryFilter,
55
- RESTFramework::OrderingFilter,
56
- RESTFramework::SearchFilter,
57
- ].freeze,
58
- filter_recordset_before_find: true,
59
- filter_fields: nil,
60
- ordering_fields: nil,
61
- ordering_query_param: "ordering".freeze,
62
- ordering_no_reorder: false,
63
- search_fields: nil,
64
- search_query_param: "search".freeze,
65
- search_ilike: false,
66
- ransack_options: nil,
67
- ransack_query_param: "q".freeze,
68
- ransack_distinct: true,
69
- ransack_distinct_query_param: "distinct".freeze,
70
-
71
- # Options for association assignment.
72
- permit_id_assignment: true,
73
- permit_nested_attributes_assignment: true,
74
-
75
- # Option for `recordset.create` vs `Model.create` behavior.
76
- create_from_recordset: true,
77
- }
78
-
79
- module ClassMethods
80
- IGNORE_VALIDATORS_WITH_KEYS = [ :if, :unless ].freeze
81
-
82
- def get_model
83
- return @model if @model
84
- return (@model = self.model) if self.model
85
-
86
- # Try to determine model from controller name.
87
- begin
88
- return @model = self.name.demodulize.chomp("Controller").singularize.constantize
89
- rescue NameError
90
- end
91
-
92
- raise RESTFramework::UnknownModelError, self
93
- end
94
-
95
- # Override to include ActiveRecord i18n-translated column names.
96
- def label_for(s)
97
- self.get_model.human_attribute_name(s, default: super)
98
- end
99
-
100
- # Get the available fields. Fallback to this controller's model columns, or an empty array. This
101
- # should always return an array of strings.
102
- def get_fields(input_fields: nil)
103
- input_fields ||= self.fields
104
-
105
- # If fields is a hash, then parse it.
106
- if input_fields.is_a?(Hash)
107
- return RESTFramework::Utils.parse_fields_hash(
108
- input_fields,
109
- self.get_model,
110
- exclude_associations: self.exclude_associations,
111
- action_text: self.enable_action_text,
112
- active_storage: self.enable_active_storage,
113
- )
114
- elsif !input_fields
115
- # Otherwise, if fields is nil, then fallback to columns.
116
- model = self.get_model
117
- return model ? RESTFramework::Utils.fields_for(
118
- model,
119
- exclude_associations: self.exclude_associations,
120
- action_text: self.enable_action_text,
121
- active_storage: self.enable_active_storage,
122
- ) : []
123
- elsif input_fields
124
- input_fields = input_fields.map(&:to_s)
125
- end
126
-
127
- input_fields
128
- end
129
-
130
- # Get a full field configuration, including defaults and inferred values.
131
- def field_configuration
132
- return @field_configuration if @field_configuration
133
-
134
- field_config = self.field_config&.with_indifferent_access || {}
135
- model = self.get_model
136
- columns = model.columns_hash
137
- column_defaults = model.column_defaults
138
- reflections = model.reflections
139
- attributes = model._default_attributes
140
- readonly_attributes = model.readonly_attributes
141
- read_only_fields = self.read_only_fields&.map(&:to_s)&.to_set || Set[]
142
- write_only_fields = self.write_only_fields&.map(&:to_s)&.to_set || Set[]
143
- hidden_fields = self.hidden_fields&.map(&:to_s)&.to_set || Set[]
144
- rich_text_association_names = model.reflect_on_all_associations(:has_one)
145
- .collect(&:name)
146
- .select { |n| n.to_s.start_with?("rich_text_") }
147
- attachment_reflections = model.attachment_reflections
148
-
149
- @field_configuration = self.get_fields.map { |f|
150
- cfg = field_config[f]&.dup || {}
151
- cfg[:label] ||= self.label_for(f)
152
-
153
- # Annotate primary key.
154
- if model.primary_key == f
155
- cfg[:primary_key] = true
156
-
157
- unless cfg.key?(:read_only)
158
- cfg[:read_only] = true
159
- end
160
- end
161
-
162
- # Annotate field mutability and display properties.
163
- cfg[:read_only] = true if f.in?(readonly_attributes) || f.in?(read_only_fields)
164
- cfg[:write_only] = true if f.in?(write_only_fields)
165
- cfg[:hidden] = true if f.in?(hidden_fields)
166
-
167
- # Raise warnings on some bad combinations of properties.
168
- if cfg[:write_only]
169
- if cfg[:read_only]
170
- Rails.logger.warn("RRF: `#{f}` write_only conflicts with read_only.")
171
- end
172
-
173
- if cfg[:hidden]
174
- Rails.logger.warn("RRF: `#{f}` write_only implies hidden.")
175
- end
176
-
177
- if cfg[:hidden_from_index]
178
- Rails.logger.warn("RRF: `#{f}` write_only implies hidden_from_index.")
179
- end
180
- end
181
-
182
- # Annotate column data.
183
- if column = columns[f]
184
- cfg[:kind] = "column"
185
- cfg[:type] ||= column.type
186
- cfg[:required] = true unless column.null
187
- end
188
-
189
- # Add default values from the model's schema.
190
- if cfg[:default].nil? && (column_default = column_defaults[f])
191
- cfg[:default] = column_default
192
- end
193
-
194
- # Add metadata from the model's attributes hash.
195
- if attributes.key?(f) && attribute = attributes[f]
196
- if cfg[:default].nil? && default = attribute.value_before_type_cast
197
- cfg[:default] = default
198
- end
199
- cfg[:kind] ||= "attribute"
200
-
201
- # Get any type information from the attribute.
202
- if type = attribute.type
203
- cfg[:type] ||= type.type if type.type
204
-
205
- # Get enum variants.
206
- if type.is_a?(ActiveRecord::Enum::EnumType)
207
- cfg[:enum_variants] = type.send(:mapping)
208
-
209
- # TranslateEnum Integration:
210
- translate_method = "translated_#{f.pluralize}"
211
- if model.respond_to?(translate_method)
212
- cfg[:enum_translations] = model.send(translate_method)
213
- end
214
- end
215
- end
216
- end
217
-
218
- # Get association metadata.
219
- if ref = reflections[f]
220
- cfg[:kind] = "association"
221
-
222
- # Determine sub-fields for associations.
223
- if ref.polymorphic?
224
- ref_columns = {}
225
- else
226
- ref_columns = ref.klass.columns_hash
227
- end
228
- cfg[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
229
- cfg[:sub_fields] = cfg[:sub_fields].map(&:to_s)
230
-
231
- # Very basic metadata about sub-fields.
232
- cfg[:sub_fields_metadata] = cfg[:sub_fields].map { |sf|
233
- v = {}
234
-
235
- if ref_columns[sf]
236
- v[:kind] = "column"
237
- else
238
- v[:kind] = "method"
239
- end
240
-
241
- next [ sf, v ]
242
- }.to_h.compact.presence
243
-
244
- # Determine if we render id/ids fields. Unfortunately, `has_one` does not provide this
245
- # interface.
246
- if self.permit_id_assignment && id_field = RESTFramework::Utils.id_field_for(f, ref)
247
- cfg[:id_field] = id_field
248
- end
249
-
250
- # Determine if we render nested attributes options.
251
- if self.permit_nested_attributes_assignment && (
252
- nested_opts = model.nested_attributes_options[f.to_sym].presence
253
- )
254
- cfg[:nested_attributes_options] = { field: "#{f}_attributes", **nested_opts }
255
- end
256
-
257
- begin
258
- cfg[:association_pk] = ref.active_record_primary_key
259
- rescue ActiveRecord::UnknownPrimaryKey
260
- end
261
-
262
- cfg[:reflection] = ref
263
- end
264
-
265
- # Determine if this is an ActionText "rich text".
266
- if :"rich_text_#{f}".in?(rich_text_association_names)
267
- cfg[:kind] = "rich_text"
268
- end
269
-
270
- # Determine if this is an ActiveStorage attachment.
271
- if ref = attachment_reflections[f]
272
- cfg[:kind] = "attachment"
273
- cfg[:attachment_type] = ref.macro
274
- end
275
-
276
- # Determine if this is just a method.
277
- if !cfg[:kind] && model.method_defined?(f)
278
- cfg[:kind] = "method"
279
- cfg[:read_only] = true if cfg[:read_only].nil?
280
- end
281
-
282
- # Collect validator options into a hash on their type, while also updating `required` based
283
- # on any presence validators.
284
- model.validators_on(f).each do |validator|
285
- kind = validator.kind
286
- options = validator.options
287
-
288
- # Reject validator if it includes keys like `:if` and `:unless` because those are
289
- # conditionally applied in a way that is not feasible to communicate via the API.
290
- next if IGNORE_VALIDATORS_WITH_KEYS.any? { |k| options.key?(k) }
291
-
292
- # Update `required` if we find a presence validator.
293
- cfg[:required] = true if kind == :presence
294
-
295
- # Resolve procs (and lambdas), and symbols for certain arguments.
296
- if options[:in].is_a?(Proc)
297
- options = options.merge(in: options[:in].call)
298
- elsif options[:in].is_a?(Symbol)
299
- options = options.merge(in: model.send(options[:in]))
300
- end
301
-
302
- cfg[:validators] ||= {}
303
- cfg[:validators][kind] ||= []
304
- cfg[:validators][kind] << options
305
- end
306
-
307
- next [ f, cfg ]
308
- }.to_h.compact.with_indifferent_access
309
- end
310
-
311
- def openapi_schema
312
- return @openapi_schema if @openapi_schema
313
-
314
- field_configuration = self.field_configuration
315
- @openapi_schema = {
316
- required: field_configuration.select { |_, cfg| cfg[:required] }.keys,
317
- type: "object",
318
- properties: field_configuration.map { |f, cfg|
319
- v = { title: cfg[:label] }
320
-
321
- if cfg[:kind] == "association"
322
- v[:type] = cfg[:reflection].collection? ? "array" : "object"
323
- elsif cfg[:kind] == "rich_text"
324
- v[:type] = "string"
325
- v[:"x-rrf-rich_text"] = true
326
- elsif cfg[:kind] == "attachment"
327
- v[:type] = "string"
328
- v[:"x-rrf-attachment"] = cfg[:attachment_type]
329
- else
330
- v[:type] = cfg[:type]
331
- end
332
-
333
- v[:readOnly] = true if cfg[:read_only]
334
- v[:default] = cfg[:default] if cfg.key?(:default)
335
-
336
- if enum_variants = cfg[:enum_variants]
337
- v[:enum] = enum_variants.keys
338
- v[:"x-rrf-enum_variants"] = enum_variants
339
- end
340
-
341
- if validators = cfg[:validators]
342
- v[:"x-rrf-validators"] = validators
343
- end
344
-
345
- v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
346
-
347
- if cfg[:reflection]
348
- v[:"x-rrf-reflection"] = {
349
- class_name: cfg[:reflection].class_name,
350
- foreign_key: cfg[:reflection].foreign_key,
351
- association_foreign_key: cfg[:reflection].association_foreign_key,
352
- association_primary_key: cfg[:reflection].association_primary_key,
353
- inverse_of: cfg[:reflection].inverse_of&.name,
354
- join_table: cfg[:reflection].join_table,
355
- }.compact
356
- v[:"x-rrf-association_pk"] = cfg[:association_pk]
357
- v[:"x-rrf-sub_fields"] = cfg[:sub_fields]
358
- v[:"x-rrf-sub_fields_metadata"] = cfg[:sub_fields_metadata]
359
- v[:"x-rrf-id_field"] = cfg[:id_field]
360
- v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
361
- end
362
-
363
- next [ f, v ]
364
- }.to_h,
365
- }
366
-
367
- @openapi_schema
368
- end
369
-
370
- def openapi_schema_name
371
- @openapi_schema_name ||= self.name.chomp("Controller").gsub("::", ".")
372
- end
373
-
374
- def openapi_paths(_routes, tag)
375
- paths = super
376
- schema_name = self.openapi_schema_name
377
-
378
- # Reference the model schema for request body and successful default responses.
379
- paths.each do |_path, actions|
380
- actions.each do |method, action|
381
- next unless action.is_a?(Hash)
382
-
383
- extra_action = action.dig("x-rrf-metadata", :extra_action)
384
-
385
- # Adjustments for builtin actions:
386
- if !extra_action && method != "options" # rubocop:disable Style/Next
387
- # Add schema to request body content types.
388
- action.dig(:requestBody, :content)&.each do |_t, v|
389
- v[:schema] = { "$ref" => "#/components/schemas/#{schema_name}" }
390
- end
391
-
392
- # Add schema to successful response body content types.
393
- action[:responses].each do |status, response|
394
- next unless status.to_s.start_with?("2")
395
-
396
- response[:content]&.each do |t, v|
397
- next if t == "text/html"
398
-
399
- v[:schema] = { "$ref" => "#/components/schemas/#{schema_name}" }
400
- end
401
- end
402
-
403
- # Translate 200->201 for the create action.
404
- if action[:summary] == "Create"
405
- action[:responses][201] = action[:responses].delete(200)
406
- end
407
-
408
- # Translate 200->204 for the destroy action.
409
- if action[:summary] == "Destroy"
410
- action[:responses][204] = action[:responses].delete(200)
411
- end
412
- end
413
- end
414
- end
415
-
416
- paths
417
- end
418
-
419
- def openapi_document(request, route_group_name, _routes)
420
- document = super
421
-
422
- # Insert schema into the document.
423
- document[:components] ||= {}
424
- document[:components][:schemas] ||= {}
425
- document[:components][:schemas][self.openapi_schema_name] = self.openapi_schema
426
-
427
- document.merge(
428
- {
429
- "x-rrf-primary_key" => self.get_model.primary_key,
430
- "x-rrf-callbacks" => self._process_action_callbacks.as_json,
431
- },
432
- )
433
- end
434
-
435
- def setup_delegation
436
- # Delegate extra actions.
437
- self.extra_actions&.each do |action, config|
438
- next unless config.is_a?(Hash) && config.dig(:metadata, :delegate)
439
- next unless self.get_model.respond_to?(action)
440
-
441
- self.define_method(action) do
442
- model = self.class.get_model
443
-
444
- if model.method(action).parameters.last&.first == :keyrest
445
- render(api: model.send(action, **params))
446
- else
447
- render(api: model.send(action))
448
- end
449
- end
450
- end
451
-
452
- # Delegate extra member actions.
453
- self.extra_member_actions&.each do |action, config|
454
- next unless config.is_a?(Hash) && config.dig(:metadata, :delegate)
455
- next unless self.get_model.method_defined?(action)
456
-
457
- self.define_method(action) do
458
- record = self.get_record
459
-
460
- if record.method(action).parameters.last&.first == :keyrest
461
- render(api: record.send(action, **params))
462
- else
463
- render(api: record.send(action))
464
- end
465
- end
466
- end
467
- end
468
-
469
- # Define any behavior to execute at the end of controller definition.
470
- # :nocov:
471
- def rrf_finalize
472
- super
473
- self.setup_delegation
474
- # self.setup_channel
475
-
476
- if RESTFramework.config.freeze_config
477
- self::RRF_BASE_MODEL_CONFIG.keys.each { |k|
478
- v = self.send(k)
479
- v.freeze if v.is_a?(Hash) || v.is_a?(Array)
480
- }
481
- end
482
- end
483
- # :nocov:
484
- end
485
-
486
2
  def self.included(base)
487
- RESTFramework::BaseControllerMixin.included(base)
488
-
489
- return unless base.is_a?(Class)
490
-
491
- base.extend(ClassMethods)
492
-
493
- # Add class attributes (with defaults) unless they already exist.
494
- RRF_BASE_MODEL_CONFIG.each do |a, default|
495
- next if base.respond_to?(a)
496
-
497
- base.class_attribute(a, default: default, instance_accessor: false)
498
- end
499
- end
500
-
501
- def get_fields
502
- self.class.get_fields(input_fields: self.class.fields)
503
- end
504
-
505
- # Get a hash of strong parameters for the current action.
506
- def get_allowed_parameters
507
- return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
508
-
509
- @_get_allowed_parameters = self.class.allowed_parameters
510
- return @_get_allowed_parameters if @_get_allowed_parameters
511
-
512
- # Assemble strong parameters.
513
- variations = []
514
- hash_variations = {}
515
- reflections = self.class.get_model.reflections
516
- @_get_allowed_parameters = self.get_fields.map { |f|
517
- f = f.to_s
518
- config = self.class.field_configuration[f]
519
-
520
- # ActionText Integration:
521
- if self.class.enable_action_text && reflections.key?("rich_text_#{f}")
522
- next f
523
- end
524
-
525
- # ActiveStorage Integration: `has_one_attached`
526
- if self.class.enable_active_storage && reflections.key?("#{f}_attachment")
527
- hash_variations[f] = ACTIVESTORAGE_KEYS
528
- next f
529
- end
530
-
531
- # ActiveStorage Integration: `has_many_attached`
532
- if self.class.enable_active_storage && reflections.key?("#{f}_attachments")
533
- hash_variations[f] = ACTIVESTORAGE_KEYS
534
- next nil
535
- end
536
-
537
- if config[:reflection]
538
- # Add `_id`/`_ids` variations for associations.
539
- if id_field = config[:id_field]
540
- if id_field.ends_with?("_ids")
541
- hash_variations[id_field] = []
542
- else
543
- variations << id_field
544
- end
545
- end
546
-
547
- # Add `_attributes` variations for associations.
548
- # TODO: Consider adjusting this based on `nested_attributes_options`.
549
- if self.class.permit_nested_attributes_assignment
550
- hash_variations["#{f}_attributes"] = (
551
- config[:sub_fields] + [ "_destroy" ]
552
- )
553
- end
554
-
555
- # Associations are not allowed to be submitted in their bare form (if they are submitted
556
- # that way, they will be translated to either id/ids or nested attributes assignment).
557
- next nil
558
- end
559
-
560
- next f
561
- }.compact
562
- @_get_allowed_parameters += variations
563
- @_get_allowed_parameters << hash_variations
564
-
565
- @_get_allowed_parameters
566
- end
567
-
568
- def get_serializer_class
569
- super || RESTFramework::NativeSerializer
570
- end
571
-
572
- # Use strong parameters to filter the request body.
573
- def get_body_params(bulk_mode: nil)
574
- data = self.request.request_parameters
575
- pk = self.class.get_model&.primary_key
576
- allowed_params = self.get_allowed_parameters
577
-
578
- # Before we filter the data, dynamically dispatch association assignment to either the id/ids
579
- # assignment ActiveRecord API or the nested assignment ActiveRecord API. Note that there is no
580
- # need to check for `permit_id_assignment` or `permit_nested_attributes_assignment` here, since
581
- # that is enforced by strong parameters generated by `get_allowed_parameters`.
582
- self.class.get_model.reflections.each do |name, ref|
583
- if payload = data[name]
584
- if payload.is_a?(Hash) || (payload.is_a?(Array) && payload.all? { |x| x.is_a?(Hash) })
585
- # Assume nested attributes assignment.
586
- attributes_key = "#{name}_attributes"
587
- data[attributes_key] = data.delete(name) unless data[attributes_key]
588
- elsif id_field = RESTFramework::Utils.id_field_for(name, ref)
589
- # Assume id/ids assignment.
590
- data[id_field] = data.delete(name) unless data[id_field]
591
- end
592
- end
593
- end
594
-
595
- # ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
596
- #
597
- # rubocop:disable Layout/LineLength
598
- #
599
- # Example base64 images (red, green, and blue squares):
600
- # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC
601
- # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNk+M9Qz0AEYBxVSF+FAAhKDveksOjmAAAAAElFTkSuQmCC
602
- # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC
603
- #
604
- # rubocop:enable Layout/LineLength
605
- has_many_attached_scalar_data = {}
606
- if self.class.enable_active_storage
607
- self.class.get_model.attachment_reflections.keys.each do |k|
608
- if data[k].is_a?(Array)
609
- data[k] = data[k].map { |v|
610
- if v.is_a?(String)
611
- v = BASE64_TRANSLATE.call(k, v)
612
-
613
- # Remember scalars because Rails strong params will remove it.
614
- if v.is_a?(String)
615
- has_many_attached_scalar_data[k] ||= []
616
- has_many_attached_scalar_data[k] << v
617
- end
618
- elsif v.is_a?(Hash)
619
- if v[:io].is_a?(String)
620
- v[:io] = StringIO.new(Base64.decode64(v[:io]))
621
- end
622
- end
623
-
624
- next v
625
- }
626
- elsif data[k].is_a?(Hash)
627
- if data[k][:io].is_a?(String)
628
- data[k][:io] = StringIO.new(Base64.decode64(data[k][:io]))
629
- end
630
- elsif data[k].is_a?(String)
631
- data[k] = BASE64_TRANSLATE.call(k, data[k])
632
- end
633
- end
634
- end
635
-
636
- # Filter the request body with strong params. If `bulk` is true, then we apply allowed
637
- # parameters to the `_json` key of the request body.
638
- body_params = if allowed_params == true
639
- ActionController::Parameters.new(data).permit!
640
- elsif bulk_mode
641
- pk = bulk_mode == :update ? [ pk ] : []
642
- ActionController::Parameters.new(data).permit({ _json: allowed_params + pk })
643
- else
644
- ActionController::Parameters.new(data).permit(*allowed_params)
645
- end
646
-
647
- # ActiveStorage Integration: Workaround for Rails strong params not allowing you to permit an
648
- # array containing a mix of scalars and hashes. This is needed for `has_many_attached`, because
649
- # API consumers must be able to provide scalar `signed_id` values for existing attachments along
650
- # with hashes for new attachments. It's worth mentioning that base64 scalars are converted to
651
- # hashes that conform to the ActiveStorage API.
652
- has_many_attached_scalar_data.each do |k, v|
653
- body_params[k].unshift(*v)
654
- end
655
-
656
- # Filter read-only fields.
657
- body_params.delete_if do |f, _|
658
- cfg = self.class.field_configuration[f]
659
- cfg && cfg[:read_only]
660
- end
661
-
662
- body_params
663
- end
664
- alias_method :get_create_params, :get_body_params
665
- alias_method :get_update_params, :get_body_params
666
-
667
- # Get the set of records this controller has access to.
668
- def get_recordset
669
- return self.class.recordset if self.class.recordset
670
-
671
- # If there is a model, return that model's default scope (all records by default).
672
- if model = self.class.get_model
673
- return model.all
674
- end
675
-
676
- nil
677
- end
678
-
679
- # Filter the recordset and return records this request has access to.
680
- def get_records
681
- data = self.get_recordset
682
-
683
- @records ||= self.class.filter_backends&.reduce(data) { |d, filter|
684
- filter.new(controller: self).filter_data(d)
685
- } || data
686
- end
687
-
688
- # Get a single record by primary key or another column, if allowed.
689
- def get_record
690
- return @record if @record
691
-
692
- find_by_key = self.class.get_model.primary_key
693
- is_pk = true
694
-
695
- # Find by another column if it's permitted.
696
- if find_by_param = self.class.find_by_query_param.presence
697
- if find_by = params[find_by_param].presence
698
- find_by_fields = self.class.find_by_fields&.map(&:to_s)
699
-
700
- if !find_by_fields || find_by.in?(find_by_fields)
701
- is_pk = false unless find_by_key == find_by
702
- find_by_key = find_by
703
- end
704
- end
705
- end
706
-
707
- # Get the recordset, filtering if configured.
708
- collection = if self.class.filter_recordset_before_find
709
- self.get_records
710
- else
711
- self.get_recordset
712
- end
713
-
714
- # Return the record. Route key is always `:id` by Rails' convention.
715
- if is_pk
716
- @record = collection.find(request.path_parameters[:id])
717
- else
718
- @record = collection.find_by!(find_by_key => request.path_parameters[:id])
719
- end
720
- end
721
-
722
- # Determine what collection to call `create` on.
723
- def create_from
724
- if self.class.create_from_recordset
725
- # Create with any properties inherited from the recordset. We exclude any `select` clauses
726
- # in case model callbacks need to call `count` on this collection, which typically raises a
727
- # SQL `SyntaxError`.
728
- self.get_recordset.except(:select)
729
- else
730
- # Otherwise, perform a "bare" insert_all.
731
- self.class.get_model
732
- end
733
- end
3
+ RESTFramework.deprecator.warn(<<~TXT).squish
4
+ BaseModelControllerMixin is deprecated; use RESTFramework::Controller and set the `model` and
5
+ `excluded_actions` class attributes instead.
6
+ TXT
734
7
 
735
- # Serialize the records, but also include any errors that might exist. This is used for bulk
736
- # actions, however we include it here so the helper is available everywhere.
737
- def bulk_serialize(records)
738
- # This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
739
- # the serializer directly. This would fail for active model serializers, but maybe we don't
740
- # care?
741
- s = RESTFramework::Utils.wrap_ams(self.get_serializer_class)
742
- records.map do |record|
743
- s.new(record, controller: self).serialize.merge!({ errors: record.errors.presence }.compact)
744
- end
8
+ base.include(RESTFramework::Controller)
9
+ base.model = RESTFramework::Utils.get_model(base)
10
+ base.excluded_actions = [
11
+ :index, :show, :create, :update, :destroy, :update_all, :destroy_all
12
+ ].freeze
745
13
  end
746
14
  end
747
15
 
748
- # Mixin for listing records.
749
16
  module RESTFramework::Mixins::ListModelMixin
750
- def index
751
- render(api: self.get_index_records)
752
- end
753
-
754
- # Get records with both filtering and pagination applied.
755
- def get_index_records
756
- records = self.get_records
17
+ def self.included(base)
18
+ RESTFramework.deprecator.warn(
19
+ "ListModelMixin is deprecated; set the `excluded_actions` class attribute instead.",
20
+ )
757
21
 
758
- # Handle pagination, if enabled.
759
- if paginator_class = self.class.paginator_class
760
- # Paginate if there is a `max_page_size`, or if there is no `page_size_query_param`, or if the
761
- # page size is not set to "0".
762
- max_page_size = self.class.max_page_size
763
- page_size_query_param = self.class.page_size_query_param
764
- if max_page_size || !page_size_query_param || params[page_size_query_param] != "0"
765
- paginator = paginator_class.new(data: records, controller: self)
766
- page = paginator.get_page
767
- serialized_page = self.serialize(page)
768
- return paginator.get_paginated_response(serialized_page)
769
- end
22
+ if base.excluded_actions
23
+ base.excluded_actions = (base.excluded_actions - [ :index ]).freeze
770
24
  end
771
-
772
- records
773
25
  end
774
26
  end
775
27
 
776
- # Mixin for showing records.
777
28
  module RESTFramework::Mixins::ShowModelMixin
778
- def show
779
- render(api: self.get_record)
29
+ def self.included(base)
30
+ RESTFramework.deprecator.warn(
31
+ "ShowModelMixin is deprecated; set the `excluded_actions` class attribute instead.",
32
+ )
33
+
34
+ if base.excluded_actions
35
+ base.excluded_actions = (base.excluded_actions - [ :show ]).freeze
36
+ end
780
37
  end
781
38
  end
782
39
 
783
- # Mixin for creating records.
784
40
  module RESTFramework::Mixins::CreateModelMixin
785
- def create
786
- render(api: self.create!, status: :created)
787
- end
41
+ def self.included(base)
42
+ RESTFramework.deprecator.warn(
43
+ "CreateModelMixin is deprecated; set the `excluded_actions` class attribute instead.",
44
+ )
788
45
 
789
- # Perform the `create!` call and return the created record.
790
- def create!
791
- self.create_from.create!(self.get_create_params)
46
+ if base.excluded_actions
47
+ base.excluded_actions = (base.excluded_actions - [ :create ]).freeze
48
+ end
792
49
  end
793
50
  end
794
51
 
795
- # Mixin for updating records.
796
52
  module RESTFramework::Mixins::UpdateModelMixin
797
- def update
798
- render(api: self.update!)
799
- end
53
+ def self.included(base)
54
+ RESTFramework.deprecator.warn(
55
+ "UpdateModelMixin is deprecated; set the `excluded_actions` class attribute instead.",
56
+ )
800
57
 
801
- # Perform the `update!` call and return the updated record.
802
- def update!
803
- record = self.get_record
804
- record.update!(self.get_update_params)
805
- record
58
+ if base.excluded_actions
59
+ base.excluded_actions = (base.excluded_actions - [ :update ]).freeze
60
+ end
806
61
  end
807
62
  end
808
63
 
809
- # Mixin for destroying records.
810
64
  module RESTFramework::Mixins::DestroyModelMixin
811
- def destroy
812
- self.destroy!
813
- render(api: "")
814
- end
65
+ def self.included(base)
66
+ RESTFramework.deprecator.warn(
67
+ "DestroyModelMixin is deprecated; set the `excluded_actions` class attribute instead.",
68
+ )
815
69
 
816
- # Perform the `destroy!` call and return the destroyed (and frozen) record.
817
- def destroy!
818
- self.get_record.destroy!
70
+ if base.excluded_actions
71
+ base.excluded_actions = (base.excluded_actions - [ :destroy ]).freeze
72
+ end
819
73
  end
820
74
  end
821
75
 
822
- # Mixin that includes show/list mixins.
823
76
  module RESTFramework::Mixins::ReadOnlyModelControllerMixin
824
- include RESTFramework::Mixins::BaseModelControllerMixin
825
-
826
- include RESTFramework::Mixins::ListModelMixin
827
- include RESTFramework::Mixins::ShowModelMixin
828
-
829
77
  def self.included(base)
830
- RESTFramework::BaseModelControllerMixin.included(base)
78
+ RESTFramework.deprecator.warn(<<~TXT).squish
79
+ ReadOnlyModelControllerMixin is deprecated; use RESTFramework::Controller and set the `model`
80
+ and `excluded_actions` class attributes instead.
81
+ TXT
82
+
83
+ base.include(RESTFramework::Controller)
84
+ base.model = RESTFramework::Utils.get_model(base)
85
+ base.excluded_actions = [ :create, :update, :destroy, :update_all, :destroy_all ].freeze
831
86
  end
832
87
  end
833
88
 
834
- # Mixin that includes all the CRUD mixins.
835
89
  module RESTFramework::Mixins::ModelControllerMixin
836
- include RESTFramework::Mixins::BaseModelControllerMixin
837
-
838
- include RESTFramework::Mixins::ListModelMixin
839
- include RESTFramework::Mixins::ShowModelMixin
840
- include RESTFramework::Mixins::CreateModelMixin
841
- include RESTFramework::Mixins::UpdateModelMixin
842
- include RESTFramework::Mixins::DestroyModelMixin
843
-
844
90
  def self.included(base)
845
- RESTFramework::BaseModelControllerMixin.included(base)
91
+ RESTFramework.deprecator.warn(<<~TXT).squish
92
+ ModelControllerMixin is deprecated; use RESTFramework::Controller and set the `model` class
93
+ attribute instead.
94
+ TXT
95
+
96
+ base.include(RESTFramework::Controller)
97
+ base.model = RESTFramework::Utils.get_model(base)
98
+ base.excluded_actions = nil
846
99
  end
847
100
  end
848
101