rest_framework 0.9.15 → 0.10.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.
@@ -2,6 +2,8 @@
2
2
  module RESTFramework::Mixins::BaseModelControllerMixin
3
3
  BASE64_REGEX = /data:(.*);base64,(.*)/
4
4
  BASE64_TRANSLATE = ->(field, value) {
5
+ return value unless BASE64_REGEX.match?(value)
6
+
5
7
  _, content_type, payload = value.match(BASE64_REGEX).to_a
6
8
  return {
7
9
  io: StringIO.new(Base64.decode64(payload)),
@@ -45,14 +47,24 @@ module RESTFramework::Mixins::BaseModelControllerMixin
45
47
  native_serializer_associations_limit_query_param: "associations_limit",
46
48
  native_serializer_include_associations_count: false,
47
49
 
48
- # Attributes for default model filtering, ordering, and searching.
49
- filterset_fields: nil,
50
+ # Attributes for filtering, ordering, and searching.
51
+ filter_backends: [
52
+ RESTFramework::QueryFilter,
53
+ RESTFramework::OrderingFilter,
54
+ RESTFramework::SearchFilter,
55
+ ],
56
+ filter_recordset_before_find: true,
57
+ filter_fields: nil,
50
58
  ordering_fields: nil,
51
59
  ordering_query_param: "ordering",
52
60
  ordering_no_reorder: false,
53
61
  search_fields: nil,
54
62
  search_query_param: "search",
55
63
  search_ilike: false,
64
+ ransack_options: nil,
65
+ ransack_query_param: "q",
66
+ ransack_distinct: true,
67
+ ransack_distinct_query_param: "distinct",
56
68
 
57
69
  # Options for association assignment.
58
70
  permit_id_assignment: true,
@@ -60,21 +72,11 @@ module RESTFramework::Mixins::BaseModelControllerMixin
60
72
 
61
73
  # Option for `recordset.create` vs `Model.create` behavior.
62
74
  create_from_recordset: true,
63
-
64
- # Control if filtering is done before find.
65
- filter_recordset_before_find: true,
66
-
67
- # Options for `ransack` filtering.
68
- ransack_options: nil,
69
- ransack_query_param: "q",
70
- ransack_distinct: true,
71
- ransack_distinct_query_param: "distinct",
72
75
  }
73
76
 
74
77
  module ClassMethods
75
78
  IGNORE_VALIDATORS_WITH_KEYS = [:if, :unless].freeze
76
79
 
77
- # Get the model for this controller.
78
80
  def get_model
79
81
  return @model if @model
80
82
  return (@model = self.model) if self.model
@@ -88,8 +90,8 @@ module RESTFramework::Mixins::BaseModelControllerMixin
88
90
  raise RESTFramework::UnknownModelError, self
89
91
  end
90
92
 
91
- # Override `get_label` to include ActiveRecord i18n-translated column names.
92
- def get_label(s)
93
+ # Override to include ActiveRecord i18n-translated column names.
94
+ def label_for(s)
93
95
  return self.get_model.human_attribute_name(s, default: super)
94
96
  end
95
97
 
@@ -101,13 +103,20 @@ module RESTFramework::Mixins::BaseModelControllerMixin
101
103
  # If fields is a hash, then parse it.
102
104
  if input_fields.is_a?(Hash)
103
105
  return RESTFramework::Utils.parse_fields_hash(
104
- input_fields, self.get_model, exclude_associations: self.exclude_associations
106
+ input_fields,
107
+ self.get_model,
108
+ exclude_associations: self.exclude_associations,
109
+ action_text: self.enable_action_text,
110
+ active_storage: self.enable_active_storage,
105
111
  )
106
112
  elsif !input_fields
107
113
  # Otherwise, if fields is nil, then fallback to columns.
108
114
  model = self.get_model
109
115
  return model ? RESTFramework::Utils.fields_for(
110
- model, exclude_associations: self.exclude_associations
116
+ model,
117
+ exclude_associations: self.exclude_associations,
118
+ action_text: self.enable_action_text,
119
+ active_storage: self.enable_active_storage,
111
120
  ) : []
112
121
  elsif input_fields
113
122
  input_fields = input_fields.map(&:to_s)
@@ -117,10 +126,10 @@ module RESTFramework::Mixins::BaseModelControllerMixin
117
126
  end
118
127
 
119
128
  # Get a field's config, including defaults.
120
- def get_field_config(f)
129
+ def field_config_for(f)
121
130
  f = f.to_sym
122
- @_get_field_config ||= {}
123
- return @_get_field_config[f] if @_get_field_config[f]
131
+ @_field_config_for ||= {}
132
+ return @_field_config_for[f] if @_field_config_for[f]
124
133
 
125
134
  config = self.field_config&.dig(f) || {}
126
135
 
@@ -147,7 +156,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
147
156
  }.to_h.compact.presence
148
157
  end
149
158
 
150
- return @_get_field_config[f] = config.compact
159
+ return @_field_config_for[f] = config.compact
151
160
  end
152
161
 
153
162
  # Get metadata about the resource's fields.
@@ -173,7 +182,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
173
182
  metadata = {
174
183
  type: nil,
175
184
  kind: nil,
176
- label: self.get_label(f),
185
+ label: self.label_for(f),
177
186
  primary_key: nil,
178
187
  required: nil,
179
188
  read_only: nil,
@@ -305,14 +314,14 @@ module RESTFramework::Mixins::BaseModelControllerMixin
305
314
  end
306
315
 
307
316
  # Serialize any field config.
308
- metadata[:config] = self.get_field_config(f).presence
317
+ metadata[:config] = self.field_config_for(f).presence
309
318
 
310
319
  next [f, metadata.compact]
311
320
  }.to_h
312
321
  end
313
322
 
314
323
  # Get a hash of metadata to be rendered in the `OPTIONS` response.
315
- def get_options_metadata
324
+ def options_metadata
316
325
  return super.merge(
317
326
  {
318
327
  primary_key: self.get_model.primary_key,
@@ -398,9 +407,8 @@ module RESTFramework::Mixins::BaseModelControllerMixin
398
407
  return self.class.get_fields(input_fields: self.class.fields)
399
408
  end
400
409
 
401
- # Pass fields to get dynamic metadata based on which fields are available.
402
- def get_options_metadata
403
- return self.class.get_options_metadata
410
+ def options_metadata
411
+ return self.class.options_metadata
404
412
  end
405
413
 
406
414
  # Get a list of parameters allowed for the current action.
@@ -410,35 +418,34 @@ module RESTFramework::Mixins::BaseModelControllerMixin
410
418
  @_get_allowed_parameters = self.allowed_parameters
411
419
  return @_get_allowed_parameters if @_get_allowed_parameters
412
420
 
413
- # For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
421
+ # Assemble strong parameters.
414
422
  variations = []
415
423
  hash_variations = {}
416
- alt_hash_variations = {}
417
424
  reflections = self.class.get_model.reflections
418
425
  @_get_allowed_parameters = self.get_fields.map { |f|
419
426
  f = f.to_s
420
427
 
421
- # ActiveStorage Integration: `has_one_attached`.
422
- if reflections.key?("#{f}_attachment")
423
- hash_variations[f] = ACTIVESTORAGE_KEYS
428
+ # ActionText Integration:
429
+ if self.class.enable_action_text && reflections.key?("rich_test_#{f}")
424
430
  next f
425
431
  end
426
432
 
427
- # ActiveStorage Integration: `has_many_attached`.
428
- if reflections.key?("#{f}_attachments")
429
- hash_variations[f] = []
430
- alt_hash_variations[f] = ACTIVESTORAGE_KEYS
431
- next nil
433
+ # ActiveStorage Integration: `has_one_attached`
434
+ if self.class.enable_active_storage && reflections.key?("#{f}_attachment")
435
+ hash_variations[f] = ACTIVESTORAGE_KEYS
436
+ next f
432
437
  end
433
438
 
434
- # ActionText Integration.
435
- if reflections.key?("rich_test_#{f}")
436
- next f
439
+ # ActiveStorage Integration: `has_many_attached`
440
+ if self.class.enable_active_storage && reflections.key?("#{f}_attachments")
441
+ hash_variations[f] = ACTIVESTORAGE_KEYS
442
+ next nil
437
443
  end
438
444
 
439
445
  # Return field if it's not an association.
440
446
  next f unless ref = reflections[f]
441
447
 
448
+ # Add `_id`/`_ids` variations for associations.
442
449
  if self.permit_id_assignment && id_field = RESTFramework::Utils.get_id_field(f, ref)
443
450
  if id_field.ends_with?("_ids")
444
451
  hash_variations[id_field] = []
@@ -447,34 +454,36 @@ module RESTFramework::Mixins::BaseModelControllerMixin
447
454
  end
448
455
  end
449
456
 
457
+ # Add `_attributes` variations for associations.
450
458
  if self.permit_nested_attributes_assignment
451
459
  hash_variations["#{f}_attributes"] = (
452
- self.class.get_field_config(f)[:sub_fields] + ["_destroy"]
460
+ self.class.field_config_for(f)[:sub_fields] + ["_destroy"]
453
461
  )
454
462
  end
455
463
 
456
- # Associations are not allowed to be submitted in their bare form.
464
+ # Associations are not allowed to be submitted in their bare form (if they are submitted that
465
+ # way, they will be translated to either ID assignment or nested attributes assignment).
457
466
  next nil
458
467
  }.compact
459
468
  @_get_allowed_parameters += variations
460
469
  @_get_allowed_parameters << hash_variations
461
- @_get_allowed_parameters << alt_hash_variations
470
+
462
471
  return @_get_allowed_parameters
463
472
  end
464
473
 
465
- # Get the configured serializer class, or `NativeSerializer` as a default.
466
- def get_serializer_class
474
+ def serializer_class
467
475
  return super || RESTFramework::NativeSerializer
468
476
  end
469
477
 
470
- # Get filtering backends, defaulting to using `ModelQueryFilter`, `ModelOrderingFilter`, and
471
- # `ModelSearchFilter`.
472
- def get_filter_backends
473
- return self.filter_backends || [
474
- RESTFramework::ModelQueryFilter,
475
- RESTFramework::ModelOrderingFilter,
476
- RESTFramework::ModelSearchFilter,
477
- ]
478
+ def apply_filters(data)
479
+ # TODO: Compatibility; remove in 1.0.
480
+ if filtered_data = self.try(:get_filtered_data, data)
481
+ return filtered_data
482
+ end
483
+
484
+ return self.filter_backends&.reduce(data) { |d, filter|
485
+ filter.new(controller: self).filter_data(d)
486
+ } || data
478
487
  end
479
488
 
480
489
  # Use strong parameters to filter the request body using the configured allowed parameters.
@@ -500,6 +509,47 @@ module RESTFramework::Mixins::BaseModelControllerMixin
500
509
  end
501
510
  end
502
511
 
512
+ # ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
513
+ #
514
+ # rubocop:disable Layout/LineLength
515
+ #
516
+ # Example base64 images (red, green, and blue squares):
517
+ # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mP8z8BQz0AEYBxVSF+FABJADveWkH6oAAAAAElFTkSuQmCC
518
+ # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNk+M9Qz0AEYBxVSF+FAAhKDveksOjmAAAAAElFTkSuQmCC
519
+ # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAFUlEQVR42mNkYPhfz0AEYBxVSF+FAP5FDvcfRYWgAAAAAElFTkSuQmCC
520
+ #
521
+ # rubocop:enable Layout/LineLength
522
+ has_many_attached_scalar_data = {}
523
+ if self.class.enable_active_storage
524
+ self.class.get_model.attachment_reflections.keys.each do |k|
525
+ if data[k].is_a?(Array)
526
+ data[k] = data[k].map { |v|
527
+ if v.is_a?(String)
528
+ v = BASE64_TRANSLATE.call(k, v)
529
+
530
+ # Remember scalars because Rails strong params will remove it.
531
+ if v.is_a?(String)
532
+ has_many_attached_scalar_data[k] ||= []
533
+ has_many_attached_scalar_data[k] << v
534
+ end
535
+ elsif v.is_a?(Hash)
536
+ if v[:io].is_a?(String)
537
+ v[:io] = StringIO.new(Base64.decode64(v[:io]))
538
+ end
539
+ end
540
+
541
+ next v
542
+ }
543
+ elsif data[k].is_a?(Hash)
544
+ if data[k][:io].is_a?(String)
545
+ data[k][:io] = StringIO.new(Base64.decode64(data[k][:io]))
546
+ end
547
+ elsif data[k].is_a?(String)
548
+ data[k] = BASE64_TRANSLATE.call(k, data[k])
549
+ end
550
+ end
551
+ end
552
+
503
553
  # Filter the request body with strong params. If `bulk` is true, then we apply allowed
504
554
  # parameters to the `_json` key of the request body.
505
555
  body_params = if allowed_params == true
@@ -511,6 +561,15 @@ module RESTFramework::Mixins::BaseModelControllerMixin
511
561
  ActionController::Parameters.new(data).permit(*allowed_params)
512
562
  end
513
563
 
564
+ # ActiveStorage Integration: Workaround for Rails strong params not allowing you to permit an
565
+ # array containing a mix of scalars and hashes. This is needed for `has_many_attached`, because
566
+ # API consumers must be able to provide scalar `signed_id` values for existing attachments along
567
+ # with hashes for new attachments. It's worth mentioning that base64 scalars are converted to
568
+ # hashes that conform to the ActiveStorage API.
569
+ has_many_attached_scalar_data.each do |k, v|
570
+ body_params[k].unshift(*v)
571
+ end
572
+
514
573
  # Filter primary key, if configured.
515
574
  if self.filter_pk_from_request_body && bulk_mode != :update
516
575
  body_params.delete(pk)
@@ -519,35 +578,6 @@ module RESTFramework::Mixins::BaseModelControllerMixin
519
578
  # Filter fields in `exclude_body_fields`.
520
579
  (self.exclude_body_fields || []).each { |f| body_params.delete(f) }
521
580
 
522
- # ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
523
- #
524
- # rubocop:disable Layout/LineLength
525
- #
526
- # Example base64 image:
527
- # data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=
528
- #
529
- # rubocop:enable Layout/LineLength
530
- self.class.get_model.attachment_reflections.keys.each do |k|
531
- if body_params[k].is_a?(Array)
532
- body_params[k] = body_params[k].map { |v|
533
- if v.is_a?(String)
534
- BASE64_TRANSLATE.call(k, v)
535
- elsif v.is_a?(ActionController::Parameters)
536
- if v[:io].is_a?(String)
537
- v[:io] = StringIO.new(Base64.decode64(v[:io]))
538
- end
539
- v
540
- end
541
- }
542
- elsif body_params[k].is_a?(ActionController::Parameters)
543
- if body_params[k][:io].is_a?(String)
544
- body_params[k][:io] = StringIO.new(Base64.decode64(body_params[k][:io]))
545
- end
546
- elsif body_params[k].is_a?(String)
547
- body_params[k] = BASE64_TRANSLATE.call(k, body_params[k])
548
- end
549
- end
550
-
551
581
  return body_params
552
582
  end
553
583
  alias_method :get_create_params, :get_body_params
@@ -567,13 +597,12 @@ module RESTFramework::Mixins::BaseModelControllerMixin
567
597
 
568
598
  # Get the records this controller has access to *after* any filtering is applied.
569
599
  def get_records
570
- return @records ||= self.get_filtered_data(self.get_recordset)
600
+ return @records ||= self.apply_filters(self.get_recordset)
571
601
  end
572
602
 
573
- # Get a single record by primary key or another column, if allowed. The return value is cached and
574
- # exposed to the view as the `@record` instance variable.
603
+ # Get a single record by primary key or another column, if allowed. The return value is memoized
604
+ # and exposed to the view as the `@record` instance variable.
575
605
  def get_record
576
- # Cache the result.
577
606
  return @record if @record
578
607
 
579
608
  find_by_key = self.class.get_model.primary_key
@@ -598,7 +627,7 @@ module RESTFramework::Mixins::BaseModelControllerMixin
598
627
  self.get_recordset
599
628
  end
600
629
 
601
- # Return the record. Route key is always `:id` by Rails convention.
630
+ # Return the record. Route key is always `:id` by Rails' convention.
602
631
  if is_pk
603
632
  return @record = collection.find(request.path_parameters[:id])
604
633
  else
@@ -625,11 +654,9 @@ module RESTFramework::Mixins::BaseModelControllerMixin
625
654
  # This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
626
655
  # the serializer directly. This would fail for active model serializers, but maybe we don't
627
656
  # care?
628
- serializer_class = self.get_serializer_class
657
+ s = RESTFramework::Utils.wrap_ams(self.serializer_class)
629
658
  serialized_records = records.map do |record|
630
- serializer_class.new(record, controller: self).serialize.merge!(
631
- {errors: record.errors.presence}.compact,
632
- )
659
+ s.new(record, controller: self).serialize.merge!({errors: record.errors.presence}.compact)
633
660
  end
634
661
 
635
662
  return serialized_records
@@ -195,7 +195,7 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
195
195
  attachment_reflections = @model.attachment_reflections
196
196
 
197
197
  fields.each do |f|
198
- field_config = @controller.class.get_field_config(f)
198
+ field_config = @controller.class.field_config_for(f)
199
199
  next if field_config[:write_only]
200
200
 
201
201
  if f.in?(column_names)
@@ -246,27 +246,38 @@ class RESTFramework::Serializers::NativeSerializer < RESTFramework::Serializers:
246
246
  includes[f] = sub_config
247
247
  includes_map[f] = f.to_sym
248
248
  end
249
- elsif ref = reflections["rich_text_#{f}"]
249
+ elsif @controller.class.enable_action_text && ref = reflections["rich_text_#{f}"]
250
250
  # ActionText Integration: Define rich text serializer method.
251
251
  includes_map[f] = :"rich_text_#{f}"
252
252
  serializer_methods[f] = f
253
253
  self.define_singleton_method(f) do |record|
254
254
  next record.send(f).to_s
255
255
  end
256
- elsif ref = attachment_reflections[f]
256
+ elsif @controller.class.enable_active_storage && ref = attachment_reflections[f]
257
257
  # ActiveStorage Integration: Define attachment serializer method.
258
258
  if ref.macro == :has_one_attached
259
259
  serializer_methods[f] = f
260
260
  includes_map[f] = {"#{f}_attachment": :blob}
261
261
  self.define_singleton_method(f) do |record|
262
- next record.send(f).attachment&.url
262
+ attached = record.send(f)
263
+ next attached.attachment ? {
264
+ filename: attached.filename,
265
+ signed_id: attached.signed_id,
266
+ url: attached.url,
267
+ } : nil
263
268
  end
264
269
  elsif ref.macro == :has_many_attached
265
270
  serializer_methods[f] = f
266
271
  includes_map[f] = {"#{f}_attachments": :blob}
267
272
  self.define_singleton_method(f) do |record|
268
273
  # Iterating the collection yields attachment objects.
269
- next record.send(f).map(&:url)
274
+ next record.send(f).map { |a|
275
+ {
276
+ filename: a.filename,
277
+ signed_id: a.signed_id,
278
+ url: a.url,
279
+ }
280
+ }
270
281
  end
271
282
  end
272
283
  elsif @model.method_defined?(f)
@@ -28,7 +28,7 @@ module RESTFramework::Utils
28
28
  [f, {}]
29
29
  }.to_h if metadata[:fields].is_a?(Array)
30
30
  metadata[:fields]&.each do |field, cfg|
31
- cfg[:label] = controller.get_label(field) unless cfg[:label]
31
+ cfg[:label] = controller.label_for(field) unless cfg[:label]
32
32
  end
33
33
  end
34
34
 
@@ -47,7 +47,7 @@ module RESTFramework::Utils
47
47
 
48
48
  # Insert action label if it's not provided.
49
49
  if controller
50
- metadata[:label] ||= controller.get_label(k)
50
+ metadata[:label] ||= controller.label_for(k)
51
51
  end
52
52
 
53
53
  next [
@@ -153,16 +153,21 @@ module RESTFramework::Utils
153
153
  end
154
154
 
155
155
  # Parse fields hashes.
156
- def self.parse_fields_hash(fields_hash, model, exclude_associations: nil)
157
- parsed_fields = fields_hash[:only] || (
158
- model ? self.fields_for(model, exclude_associations: exclude_associations) : []
156
+ def self.parse_fields_hash(h, model, exclude_associations:, action_text:, active_storage:)
157
+ parsed_fields = h[:only] || (
158
+ model ? self.fields_for(
159
+ model,
160
+ exclude_associations: exclude_associations,
161
+ action_text: action_text,
162
+ active_storage: active_storage,
163
+ ) : []
159
164
  )
160
- parsed_fields += fields_hash[:include].map(&:to_s) if fields_hash[:include]
161
- parsed_fields -= fields_hash[:exclude].map(&:to_s) if fields_hash[:exclude]
162
- parsed_fields -= fields_hash[:except].map(&:to_s) if fields_hash[:except]
165
+ parsed_fields += h[:include].map(&:to_s) if h[:include]
166
+ parsed_fields -= h[:exclude].map(&:to_s) if h[:exclude]
167
+ parsed_fields -= h[:except].map(&:to_s) if h[:except]
163
168
 
164
169
  # Warn for any unknown keys.
165
- (fields_hash.keys - [:only, :except, :include, :exclude]).each do |k|
170
+ (h.keys - [:only, :except, :include, :exclude]).each do |k|
166
171
  Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
167
172
  end
168
173
 
@@ -173,16 +178,24 @@ module RESTFramework::Utils
173
178
  # Get the fields for a given model, including not just columns (which includes
174
179
  # foreign keys), but also associations. Note that we always return an array of
175
180
  # strings, not symbols.
176
- def self.fields_for(model, exclude_associations: nil)
181
+ def self.fields_for(model, exclude_associations:, action_text:, active_storage:)
177
182
  foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
178
183
  base_fields = model.column_names.reject { |c| c.in?(foreign_keys) }
179
184
 
180
185
  return base_fields if exclude_associations
181
186
 
182
- # Add associations in addition to normal columns.
183
- return base_fields + model.reflections.map { |association, ref|
187
+ # ActionText Integration: Determine the normalized field names for action text attributes.
188
+ atf = action_text ? model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
189
+ n.to_s.start_with?("rich_text_")
190
+ }.map { |n| n.to_s.delete_prefix("rich_text_") } : []
191
+
192
+ # ActiveStorage Integration: Determine the normalized field names for active storage attributes.
193
+ asf = active_storage ? model.attachment_reflections.keys : []
194
+
195
+ # Associations:
196
+ associations = model.reflections.map { |association, ref|
184
197
  # Ignore associations for which we have custom integrations.
185
- if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob ActionText::RichText))
198
+ if ref.class_name.in?(%w(ActionText::RichText ActiveStorage::Attachment ActiveStorage::Blob))
186
199
  next nil
187
200
  end
188
201
 
@@ -193,11 +206,9 @@ module RESTFramework::Utils
193
206
  end
194
207
 
195
208
  next association
196
- }.compact + model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
197
- n.to_s.start_with?("rich_text_")
198
- }.map { |n|
199
- n.to_s.delete_prefix("rich_text_")
200
- } + model.attachment_reflections.keys
209
+ }.compact
210
+
211
+ return base_fields + associations + atf + asf
201
212
  end
202
213
 
203
214
  # Get the sub-fields that may be serialized and filtered/ordered for a reflection.
@@ -234,4 +245,13 @@ module RESTFramework::Utils
234
245
 
235
246
  return nil
236
247
  end
248
+
249
+ # Wrap a serializer with an adapter if it is an ActiveModel::Serializer.
250
+ def self.wrap_ams(s)
251
+ if defined?(ActiveModel::Serializer) && (s < ActiveModel::Serializer)
252
+ return RESTFramework::ActiveModelSerializerAdapterFactory.for(s)
253
+ end
254
+
255
+ return s
256
+ end
237
257
  end
@@ -31,57 +31,57 @@ module RESTFramework
31
31
  EXTERNAL_ASSETS = {
32
32
  # Bootstrap
33
33
  "bootstrap.min.css" => {
34
- url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/css/bootstrap.min.css",
35
- sri: "sha384-aFq/bzH65dt+w6FI2ooMVUpc+21e0SRygnTpmBvdBgSdnuTN7QbdgL+OapgHtvPp",
34
+ url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css",
35
+ sri: "sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH",
36
36
  },
37
37
  "bootstrap.min.js" => {
38
- url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha2/dist/js/bootstrap.bundle.min.js",
39
- sri: "sha384-qKXV1j0HvMUeCBQ+QVp7JcfGl760yU08IQ+GpUo5hlbpg51QRiuqHAJz8+BrxE/N",
38
+ url: "https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js",
39
+ sri: "sha384-YvpcrYf0tY3lHB60NNkmXc5s9fDVZLESaAA55NDzOxhy9GkcIdslK1eN7N6jIeHz",
40
40
  },
41
41
 
42
42
  # Bootstrap Icons
43
43
  "bootstrap-icons.min.css" => {
44
- url: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.4/font/bootstrap-icons.min.css",
44
+ url: "https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css",
45
45
  inline_fonts: true,
46
46
  },
47
47
 
48
48
  # Highlight.js
49
49
  "highlight.min.js" => {
50
- url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js",
51
- sri: "sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA==",
50
+ url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/highlight.min.js",
51
+ sri: "sha512-6QBAC6Sxc4IF04SvIg0k78l5rP5YgVjmHX2NeArelbxM3JGj4imMqfNzEta3n+mi7iG3nupdLnl3QrbfjdXyTg==",
52
52
  },
53
53
  "highlight-json.min.js" => {
54
- url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js",
55
- sri: "sha512-0xYvyncS9OLE7GOpNBZFnwyh9+bq4HVgk4yVVYI678xRvE22ASicF1v6fZ1UiST+M6pn17MzFZdvVCI3jTHSyw==",
54
+ url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/languages/json.min.js",
55
+ sri: "sha512-8JO7/pRnd1Ce8OBXWQg85e5wNPJdBaQdN8w4oDa+HelMXaLwCxTdbzdWHmJtWR9AmcI6dOln4FS5/KrzpxqqfQ==",
56
56
  },
57
57
  "highlight-xml.min.js" => {
58
- url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/xml.min.js",
59
- sri: "sha512-5zBcw+OKRkaNyvUEPlTSfYylVzgpi7KpncY36b0gRudfxIYIH0q0kl2j26uCUB3YBRM6ytQQEZSgRg+ZlBTmdA==",
58
+ url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/languages/xml.min.js",
59
+ sri: "sha512-/vq6wbS2Qkv8Hj4mP3Jd/m6MbnIrquzZiUt9tIluQfe332IQeFDrSIK7j2cjAyn6/9Ntb2WMPbo1CAxu26NViA==",
60
60
  },
61
61
  "highlight-a11y-dark.min.css" => {
62
- url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-dark.min.css",
62
+ url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/a11y-dark.min.css",
63
63
  sri: "sha512-Vj6gPCk8EZlqnoveEyuGyYaWZ1+jyjMPg8g4shwyyNlRQl6d3L9At02ZHQr5K6s5duZl/+YKMnM3/8pDhoUphg==",
64
64
  extra_tag_attrs: {class: "rrf-dark-mode"},
65
65
  },
66
66
  "highlight-a11y-light.min.css" => {
67
- url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/a11y-light.min.css",
67
+ url: "https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.0/styles/a11y-light.min.css",
68
68
  sri: "sha512-WDk6RzwygsN9KecRHAfm9HTN87LQjqdygDmkHSJxVkVI7ErCZ8ZWxP6T8RvBujY1n2/E4Ac+bn2ChXnp5rnnHA==",
69
69
  extra_tag_attrs: {class: "rrf-light-mode"},
70
70
  },
71
71
 
72
72
  # NeatJSON
73
73
  "neatjson.min.js" => {
74
- url: "https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js",
74
+ url: "https://cdn.jsdelivr.net/npm/neatjson@0.10.6/javascript/neatjson.min.js",
75
75
  exclude_from_docs: true,
76
76
  },
77
77
 
78
78
  # Trix
79
79
  "trix.min.css" => {
80
- url: "https://unpkg.com/trix@2.0.0/dist/trix.css",
80
+ url: "https://unpkg.com/trix@2.0.8/dist/trix.css",
81
81
  exclude_from_docs: true,
82
82
  },
83
83
  "trix.min.js" => {
84
- url: "https://unpkg.com/trix@2.0.0/dist/trix.umd.min.js",
84
+ url: "https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js",
85
85
  exclude_from_docs: true,
86
86
  },
87
87
  }.map { |name, cfg|