rest_framework 0.9.15 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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|