rest_framework 0.7.4 → 0.7.6

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e37d7baf0b2e805e4df451a8ef646e57ed078a7edd3f9d03b524198eef9fc83f
4
- data.tar.gz: c6f5bf3f8f7bfa79566a1292f7634f94b29a09a934a0179adea6e3320291cb9e
3
+ metadata.gz: edeb7963dbca0185ff384e628a185c83341e3154d6414c9d8f70a05115abd23a
4
+ data.tar.gz: 8d6bc94fea07e57c20625c8aa192ced88789e078bf3a4b29b71e6c80099f2436
5
5
  SHA512:
6
- metadata.gz: 13b7905e307bbc0f204d2570c05298cf9c4bcc025719210c3f496bac64a387f611ec4ab3772f8c268cc41b608973b7d3502cf5542d9fee0cce5587ef80a773cc
7
- data.tar.gz: 49ef1e4a637fa2f3dee50f40042cdf50a28f68f632a0e5587a339e492954ef90c1f7cd8d80f4b0fb39668838ee020556b29dfd806377e82541b3c4a8940d6b55
6
+ metadata.gz: 68ecf8a8a045c3e217c597f86b46573dc80bb931e20be45af01b939079f8cf2dd27d68e8bebd8d458726b04b8b55fb2dd65a60bcf4c847c30feb1257f7cfb9ea
7
+ data.tar.gz: 388478f7c8f2c51a96740f25043aa7d188b5e59731478f4bca2ce6171fadc5ba1e201b12ee051859386ce151c1114b9b6f207dbe840e732dec652d9639323825
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.7.4
1
+ 0.7.6
@@ -3,8 +3,20 @@
3
3
  <%= csrf_meta_tags %>
4
4
  <%= csp_meta_tag rescue nil %>
5
5
 
6
+ <!-- Bootstrap -->
6
7
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
7
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/styles/vs.min.css" integrity="sha512-aWjgJTbdG4imzxTxistV5TVNffcYGtIQQm2NBNahV6LmX14Xq9WwZTL1wPjaSglUuVzYgwrq+0EuI4+vKvQHHw==" crossorigin="anonymous">
8
+ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
9
+
10
+ <!-- Highlight.js -->
11
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/vs.min.css" integrity="sha512-AVoZ71dJLtHRlsgWwujPT1hk2zxtFWsPlpTPCc/1g0WgpbmlzkqlDFduAvnOV4JJWKUquPc1ZyMc5eq4fRnKOQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
12
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js" integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
13
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js" integrity="sha512-0xYvyncS9OLE7GOpNBZFnwyh9+bq4HVgk4yVVYI678xRvE22ASicF1v6fZ1UiST+M6pn17MzFZdvVCI3jTHSyw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
14
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/xml.min.js" integrity="sha512-5zBcw+OKRkaNyvUEPlTSfYylVzgpi7KpncY36b0gRudfxIYIH0q0kl2j26uCUB3YBRM6ytQQEZSgRg+ZlBTmdA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
15
+
16
+ <!-- NeatJSON -->
17
+ <script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
18
+
19
+ <!-- Custom Style -->
8
20
  <style>
9
21
  /* Adjust headers to always take up their entire row, and tweak the sizing. */
10
22
  h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
@@ -17,7 +29,7 @@ h6 { font-size: 1rem; }
17
29
 
18
30
  /* Make code and code blocks a little nicer looking. */
19
31
  code {
20
- padding: 0 .35em;
32
+ padding: .5em !important;
21
33
  background-color: #f3f3f3 !important;
22
34
  border: 1px solid #aaa;
23
35
  border-radius: 3px;
@@ -56,25 +68,22 @@ code {
56
68
  }
57
69
  </style>
58
70
 
59
- <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
60
- <script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
61
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/highlight.min.js" integrity="sha512-TDKKr+IvoqZnPzc3l35hdjpHD0m+b2EC2SrLEgKDRWpxf2rFCxemkgvJ5kfU48ip+Y+m2XVKyOCD85ybtlZDmw==" crossorigin="anonymous"></script>
62
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/json.min.js" integrity="sha512-FoN8JE+WWCdIGXAIT8KQXwpiavz0Mvjtfk7Rku3MDUNO0BDCiRMXAsSX+e+COFyZTcDb9HDgP+pM2RX12d4j+A==" crossorigin="anonymous"></script>
63
- <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/xml.min.js" integrity="sha512-dICltIgnUP+QSJrnYGCV8943p3qSDgvcg2NU4W8IcOZP4tdrvxlXjbhIznhtVQEcXow0mOjLM0Q6/NvZsmUH4g==" crossorigin="anonymous"></script>
71
+ <!-- Custom JavaScript -->
64
72
  <script>
65
- hljs.initHighlightingOnLoad()
66
-
67
73
  // What to do when document loads.
68
74
  document.addEventListener("DOMContentLoaded", (event) => {
69
75
  // Pretty-print JSON.
70
76
  [...document.getElementsByClassName("language-json")].forEach((element, index) => {
71
- element.innerHTML = neatJSON(JSON.parse(element.innerHTML), {
77
+ element.innerHTML = neatJSON(JSON.parse(element.innerText), {
72
78
  wrap: 80,
73
79
  afterComma: 1,
74
80
  afterColon: 1,
75
81
  })
76
82
  });
77
83
 
84
+ // Then highlight it.
85
+ hljs.highlightAll();
86
+
78
87
  // Insert copy link and callback to copy contents of `<code>` element.
79
88
  [...document.getElementsByClassName("rrf-copy")].forEach((element, index) => {
80
89
  element.insertAdjacentHTML(
@@ -12,16 +12,15 @@ module RESTFramework::BaseControllerMixin
12
12
  :created_at, :created_by, :created_by_id, :updated_at, :updated_by, :updated_by_id
13
13
  ].freeze,
14
14
  accept_generic_params_as_body_params: false,
15
- show_backtrace: false,
16
15
  extra_actions: nil,
17
16
  extra_member_actions: nil,
18
17
  filter_backends: nil,
19
18
  singleton_controller: nil,
20
19
 
21
- # Metadata and display options.
20
+ # Options related to metadata and display.
22
21
  title: nil,
23
22
  description: nil,
24
- inflect_acronyms: ["ID", "REST", "API"].freeze,
23
+ inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
25
24
 
26
25
  # Options related to serialization.
27
26
  rescue_unknown_format_with: :json,
@@ -36,6 +35,10 @@ module RESTFramework::BaseControllerMixin
36
35
  page_size_query_param: "page_size",
37
36
  max_page_size: nil,
38
37
 
38
+ # Options related to bulk actions and batch processing.
39
+ bulk_guard_query_param: nil,
40
+ enable_batch_processing: nil,
41
+
39
42
  # Option to disable serializer adapters by default, mainly introduced because Active Model
40
43
  # Serializers will do things like serialize `[]` into `{"":[]}`.
41
44
  disable_adapters_by_default: true,
@@ -74,6 +77,13 @@ module RESTFramework::BaseControllerMixin
74
77
  end
75
78
  end
76
79
 
80
+ # Add builtin bulk actions.
81
+ RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
82
+ if self.method_defined?(action)
83
+ actions[action] = {path: "", methods: methods, type: :builtin}
84
+ end
85
+ end
86
+
77
87
  # Add extra actions.
78
88
  if extra_actions = self.try(:extra_actions)
79
89
  actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions, controller: self))
@@ -159,11 +169,17 @@ module RESTFramework::BaseControllerMixin
159
169
  end
160
170
 
161
171
  # Handle some common exceptions.
162
- base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
163
- base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
164
- base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
165
- base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
166
- base.rescue_from(ActiveModel::UnknownAttributeError, with: :unknown_attribute_error)
172
+ unless RESTFramework.config.disable_rescue_from
173
+ base.rescue_from(
174
+ ActiveRecord::RecordNotFound,
175
+ ActiveRecord::RecordInvalid,
176
+ ActiveRecord::RecordNotSaved,
177
+ ActiveRecord::RecordNotDestroyed,
178
+ ActiveRecord::RecordNotUnique,
179
+ ActiveModel::UnknownAttributeError,
180
+ with: :rrf_error_handler,
181
+ )
182
+ end
167
183
 
168
184
  # Use `TracePoint` hook to automatically call `rrf_finalize`.
169
185
  unless RESTFramework.config.disable_auto_finalize
@@ -211,6 +227,7 @@ module RESTFramework::BaseControllerMixin
211
227
 
212
228
  # Filter an arbitrary data set over all configured filter backends.
213
229
  def get_filtered_data(data)
230
+ # Apply each filter sequentially.
214
231
  self.get_filter_backends.each do |filter_class|
215
232
  filter = filter_class.new(controller: self)
216
233
  data = filter.get_filtered_data(data)
@@ -223,53 +240,26 @@ module RESTFramework::BaseControllerMixin
223
240
  return self.class.get_options_metadata
224
241
  end
225
242
 
226
- def record_invalid(e)
227
- return api_response(
228
- {
229
- message: "Record invalid.", errors: e.record&.errors
230
- }.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
231
- status: 400,
232
- )
233
- end
234
-
235
- def record_not_found(e)
236
- return api_response(
237
- {
238
- message: "Record not found.",
239
- }.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
240
- status: 404,
241
- )
242
- end
243
-
244
- def record_not_saved(e)
245
- return api_response(
246
- {
247
- message: "Record not saved.", errors: e.record&.errors
248
- }.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
249
- status: 400,
250
- )
251
- end
252
-
253
- def record_not_destroyed(e)
254
- return api_response(
255
- {
256
- message: "Record not destroyed.", errors: e.record&.errors
257
- }.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
258
- status: 400,
259
- )
260
- end
243
+ def rrf_error_handler(e)
244
+ status = case e
245
+ when ActiveRecord::RecordNotFound
246
+ 404
247
+ else
248
+ 400
249
+ end
261
250
 
262
- def unknown_attribute_error(e)
263
251
  return api_response(
264
252
  {
265
- message: e.message.capitalize,
266
- }.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
267
- status: 400,
253
+ message: e.message,
254
+ errors: e.try(:record).try(:errors),
255
+ exception: RESTFramework.config.show_backtrace ? e.full_message : nil,
256
+ }.compact,
257
+ status: status,
268
258
  )
269
259
  end
270
260
 
271
- # Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
272
- # with support or passing custom `kwargs` to the underlying `render` calls.
261
+ # Render a browsable API for `html` format, along with basic `json`/`xml` formats, and with
262
+ # support or passing custom `kwargs` to the underlying `render` calls.
273
263
  def api_response(payload, html_kwargs: nil, **kwargs)
274
264
  html_kwargs ||= {}
275
265
  json_kwargs = kwargs.delete(:json_kwargs) || {}
@@ -0,0 +1,82 @@
1
+ require_relative "models"
2
+
3
+ # Mixin for creating records in bulk. This is unique compared to update/destroy because we overload
4
+ # the existing `create` action to support bulk creation.
5
+ # :nocov:
6
+ module RESTFramework::BulkCreateModelMixin
7
+ def create
8
+ status, payload = self.create_all!
9
+ return api_response(payload, status: status)
10
+ end
11
+
12
+ # Perform the `create` or `insert_all` call and return the created records with any errors. The
13
+ # result should be of the form: `(status, payload)`, and `payload` should be of the form:
14
+ # `[{success:, record: | errors:}]`, unless batch mode is enabled, in which case `payload` is
15
+ # blank with a status of `202`.
16
+ def create_all!
17
+ if self.class.bulk_batch_mode
18
+ insert_from = if self.get_recordset.respond_to?(:insert_all) && self.create_from_recordset
19
+ # Create with any properties inherited from the recordset. We exclude any `select` clauses
20
+ # in case model callbacks need to call `count` on this collection, which typically raises a
21
+ # SQL `SyntaxError`.
22
+ self.get_recordset.except(:select)
23
+ else
24
+ # Otherwise, perform a "bare" insert_all.
25
+ self.class.get_model
26
+ end
27
+
28
+ insert_from
29
+ end
30
+
31
+ # Perform bulk creation, possibly in a transaction.
32
+ self.class._rrf_bulk_transaction do
33
+ if self.get_recordset.respond_to?(:insert_all) && self.create_from_recordset
34
+ # Create with any properties inherited from the recordset. We exclude any `select` clauses
35
+ # in case model callbacks need to call `count` on this collection, which typically raises a
36
+ # SQL `SyntaxError`.
37
+ return self.get_recordset.except(:select).create!(self.get_create_params)
38
+ else
39
+ # Otherwise, perform a "bare" insert_all.
40
+ return self.class.get_model.insert_all(self.get_create_params)
41
+ end
42
+ end
43
+ end
44
+ end
45
+
46
+ # Mixin for updating records in bulk.
47
+ module RESTFramework::BulkUpdateModelMixin
48
+ def update_all
49
+ raise NotImplementedError, "TODO"
50
+ end
51
+
52
+ # Perform the `update!` call and return the updated record.
53
+ def update_all!
54
+ raise NotImplementedError, "TODO"
55
+ end
56
+ end
57
+
58
+ # Mixin for destroying records in bulk.
59
+ module RESTFramework::BulkDestroyModelMixin
60
+ def destroy_all
61
+ raise NotImplementedError, "TODO"
62
+ end
63
+
64
+ # Perform the `destroy!` call and return the destroyed (and frozen) record.
65
+ def destroy_all!
66
+ raise NotImplementedError, "TODO"
67
+ end
68
+ end
69
+
70
+ # Mixin that includes all the CRUD bulk mixins.
71
+ module RESTFramework::BulkModelControllerMixin
72
+ include RESTFramework::ModelControllerMixin
73
+
74
+ include RESTFramework::BulkCreateModelMixin
75
+ include RESTFramework::BulkUpdateModelMixin
76
+ include RESTFramework::BulkDestroyModelMixin
77
+
78
+ def self.included(base)
79
+ RESTFramework::ModelControllerMixin.included(base)
80
+ end
81
+ end
82
+ # :nocov:
@@ -12,6 +12,7 @@ module RESTFramework::BaseModelControllerMixin
12
12
 
13
13
  # Attributes for configuring record fields.
14
14
  fields: nil,
15
+ field_config: nil,
15
16
  action_fields: nil,
16
17
 
17
18
  # Attributes for finding records.
@@ -38,9 +39,22 @@ module RESTFramework::BaseModelControllerMixin
38
39
  search_query_param: "search",
39
40
  search_ilike: false,
40
41
 
41
- # Other misc attributes.
42
- create_from_recordset: true, # Option for `recordset.create` vs `Model.create` behavior.
43
- filter_recordset_before_find: true, # Control if filtering is done before find.
42
+ # Option for `recordset.create` vs `Model.create` behavior.
43
+ create_from_recordset: true,
44
+
45
+ # Control if filtering is done before find.
46
+ filter_recordset_before_find: true,
47
+
48
+ # Option to exclude associations from default fields.
49
+ exclude_associations: false,
50
+
51
+ # Control if bulk operations are done in a transaction and rolled back on error, or if all bulk
52
+ # operations are attempted and errors simply returned in the response.
53
+ bulk_transactional: false,
54
+
55
+ # Control if bulk operations should be done in "batch" mode, using efficient queries, but also
56
+ # skipping model validations/callbacks.
57
+ bulk_batch_mode: false,
44
58
  }
45
59
 
46
60
  module ClassMethods
@@ -77,14 +91,54 @@ module RESTFramework::BaseModelControllerMixin
77
91
  return self.get_model.human_attribute_name(s, default: super)
78
92
  end
79
93
 
94
+ # Get fields without any action context. Always fallback to columns at the class level.
95
+ def get_fields
96
+ if self.fields.is_a?(Hash)
97
+ return RESTFramework::Utils.parse_fields_hash(
98
+ self.fields, self.get_model, exclude_associations: self.exclude_associations
99
+ )
100
+ end
101
+
102
+ return self.fields || (
103
+ self.get_model ? RESTFramework::Utils.fields_for(
104
+ self.get_model, exclude_associations: self.exclude_associations
105
+ ) : []
106
+ )
107
+ end
108
+
109
+ # Get a field's config, including defaults.
110
+ def get_field_config(f)
111
+ config = self.field_config&.dig(f.to_sym) || {}
112
+
113
+ # Default sub-fields if field is an association.
114
+ if ref = self.get_model.reflections[f]
115
+ model = ref.klass
116
+ columns = model.columns_hash
117
+ config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
118
+
119
+ # Serialize very basic metadata about sub-fields.
120
+ config[:sub_fields_metadata] = config[:sub_fields].map { |sf|
121
+ v = {}
122
+
123
+ if columns[sf]
124
+ v[:kind] = "column"
125
+ end
126
+
127
+ next [sf, v]
128
+ }.to_h.compact.presence
129
+ end
130
+
131
+ return config.compact
132
+ end
133
+
80
134
  # Get metadata about the resource's fields.
81
- def get_fields_metadata(fields: nil)
135
+ def get_fields_metadata
82
136
  # Get metadata sources.
83
137
  model = self.get_model
84
- fields ||= self.get_fields
85
- fields = fields.map(&:to_s)
138
+ fields = self.get_fields.map(&:to_s)
86
139
  columns = model.columns_hash
87
140
  column_defaults = model.column_defaults
141
+ reflections = model.reflections
88
142
  attributes = model._default_attributes
89
143
 
90
144
  return fields.map { |f|
@@ -105,9 +159,9 @@ module RESTFramework::BaseModelControllerMixin
105
159
 
106
160
  # Determine `type`, `required`, `label`, and `kind` based on schema.
107
161
  if column = columns[f]
162
+ metadata[:kind] = "column"
108
163
  metadata[:type] = column.type
109
164
  metadata[:required] = true unless column.null
110
- metadata[:kind] = "column"
111
165
  end
112
166
 
113
167
  # Determine `default` based on schema; we use `column_defaults` rather than `columns_hash`
@@ -117,25 +171,46 @@ module RESTFramework::BaseModelControllerMixin
117
171
  metadata[:default] = column_default
118
172
  end
119
173
 
120
- # Determine `default` and `kind` based on attribute only if not determined by the DB.
174
+ # Extract details from the model's attributes hash.
121
175
  if attributes.key?(f) && attribute = attributes[f]
122
176
  unless metadata.key?(:default)
123
177
  default = attribute.value_before_type_cast
124
178
  metadata[:default] = default unless default.nil?
125
179
  end
180
+ metadata[:kind] ||= "attribute"
181
+
182
+ # Get any type information from the attribute.
183
+ if type = attribute.type
184
+ metadata[:type] ||= type.type
126
185
 
127
- unless metadata[:kind]
128
- metadata[:kind] = "attribute"
186
+ # Get enum variants.
187
+ if type.is_a?(ActiveRecord::Enum::EnumType)
188
+ metadata[:enum_variants] = type.send(:mapping)
189
+ end
129
190
  end
130
191
  end
131
192
 
132
- # Determine if `kind` is a association or method if not determined already.
133
- unless metadata[:kind]
134
- if association = model.reflections[f]
135
- metadata[:kind] = "association.#{association.macro}"
136
- elsif model.method_defined?(f)
137
- metadata[:kind] = "method"
193
+ # Get association metadata.
194
+ if ref = reflections[f]
195
+ metadata[:kind] = "association"
196
+ begin
197
+ pk = ref.active_record_primary_key
198
+ rescue ActiveRecord::UnknownPrimaryKey
138
199
  end
200
+ metadata[:association] = {
201
+ macro: ref.macro,
202
+ class_name: ref.class_name,
203
+ foreign_key: ref.foreign_key,
204
+ primary_key: pk,
205
+ polymorphic: ref.polymorphic?,
206
+ table_name: ref.table_name,
207
+ options: ref.options.presence,
208
+ }.compact
209
+ end
210
+
211
+ # Determine if this is just a method.
212
+ if model.method_defined?(f)
213
+ metadata[:kind] ||= "method"
139
214
  end
140
215
 
141
216
  # Collect validator options into a hash on their type, while also updating `required` based
@@ -156,32 +231,18 @@ module RESTFramework::BaseModelControllerMixin
156
231
  metadata[:validators][kind] << options
157
232
  end
158
233
 
159
- next [f, metadata.compact]
160
- }.to_h
161
- end
234
+ # Serialize any field config.
235
+ metadata[:config] = self.get_field_config(f).presence
162
236
 
163
- # Get metadata about the resource's associations (reflections).
164
- def get_associations_metadata
165
- return self.get_model.reflections.map { |k, v|
166
- next [k, {
167
- macro: v.macro,
168
- label: self.get_label(k),
169
- class_name: v.class_name,
170
- foreign_key: v.foreign_key,
171
- primary_key: v.active_record_primary_key,
172
- polymorphic: v.polymorphic?,
173
- table_name: v.table_name,
174
- options: v.options,
175
- }.compact]
237
+ next [f, metadata.compact]
176
238
  }.to_h
177
239
  end
178
240
 
179
241
  # Get a hash of metadata to be rendered in the `OPTIONS` response. Cache the result.
180
- def get_options_metadata(fields: nil)
242
+ def get_options_metadata
181
243
  return super().merge(
182
244
  {
183
- fields: self.get_fields_metadata(fields: fields),
184
- associations: self.get_associations_metadata,
245
+ fields: self.get_fields_metadata,
185
246
  },
186
247
  )
187
248
  end
@@ -238,9 +299,10 @@ module RESTFramework::BaseModelControllerMixin
238
299
  end
239
300
 
240
301
  def self.included(base)
302
+ RESTFramework::BaseControllerMixin.included(base)
303
+
241
304
  return unless base.is_a?(Class)
242
305
 
243
- RESTFramework::BaseControllerMixin.included(base)
244
306
  base.extend(ClassMethods)
245
307
 
246
308
  # Add class attributes (with defaults) unless they already exist.
@@ -265,27 +327,23 @@ module RESTFramework::BaseModelControllerMixin
265
327
  return (action_config[action] if action) || self.class.send(generic_config_key)
266
328
  end
267
329
 
268
- # Get fields without any action context. Always fallback to columns at the class level.
269
- def self.get_fields
270
- if self.fields.is_a?(Hash)
271
- return RESTFramework::Utils.parse_fields_hash(self.fields, self.get_model)
272
- end
273
-
274
- return self.fields || self.get_model&.column_names || []
275
- end
276
-
277
330
  # Get a list of fields for the current action. Returning `nil` indicates that anything should be
278
331
  # accepted unless `fallback` is true, in which case we should fallback to this controller's model
279
332
  # columns, or en empty array.
280
333
  def get_fields(fallback: false)
281
334
  fields = _get_specific_action_config(:action_fields, :fields)
282
335
 
283
- # If fields is a hash, then parse using columns as a base, respecting `only` and `except`.
336
+ # If fields is a hash, then parse it.
284
337
  if fields.is_a?(Hash)
285
- return RESTFramework::Utils.parse_fields_hash(fields, self.class.get_model)
338
+ return RESTFramework::Utils.parse_fields_hash(
339
+ fields, self.class.get_model, exclude_associations: self.class.exclude_associations
340
+ )
286
341
  elsif !fields && fallback
287
342
  # Otherwise, if fields is nil and fallback is true, then fallback to columns.
288
- return self.class.get_model&.column_names || []
343
+ model = self.class.get_model
344
+ return model ? RESTFramework::Utils.fields_for(
345
+ model, exclude_associations: self.class.exclude_associations
346
+ ) : []
289
347
  end
290
348
 
291
349
  return fields
@@ -293,7 +351,7 @@ module RESTFramework::BaseModelControllerMixin
293
351
 
294
352
  # Pass fields to get dynamic metadata based on which fields are available.
295
353
  def get_options_metadata
296
- return self.class.get_options_metadata(fields: self.get_fields(fallback: true))
354
+ return self.class.get_options_metadata
297
355
  end
298
356
 
299
357
  # Get a list of find_by fields for the current action. Do not fallback to columns in case the user
@@ -311,12 +369,12 @@ module RESTFramework::BaseModelControllerMixin
311
369
  ) || self.get_fields
312
370
  end
313
371
 
314
- # Helper to get the configured serializer class, or `NativeSerializer` as a default.
372
+ # Get the configured serializer class, or `NativeSerializer` as a default.
315
373
  def get_serializer_class
316
374
  return super || RESTFramework::NativeSerializer
317
375
  end
318
376
 
319
- # Helper to get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
377
+ # Get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
320
378
  def get_filter_backends
321
379
  return self.class.filter_backends || [
322
380
  RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
@@ -324,14 +382,16 @@ module RESTFramework::BaseModelControllerMixin
324
382
  end
325
383
 
326
384
  # Filter the request body for keys in current action's allowed_parameters/fields config.
327
- def get_body_params
385
+ def get_body_params(data: nil)
386
+ data ||= request.request_parameters
387
+
328
388
  # Filter the request body and map to strings. Return all params if we cannot resolve a list of
329
389
  # allowed parameters or fields.
330
390
  allowed_params = self.get_allowed_parameters&.map(&:to_s)
331
391
  body_params = if allowed_params
332
- request.request_parameters.select { |p| allowed_params.include?(p) }
392
+ data.select { |p| allowed_params.include?(p) }
333
393
  else
334
- request.request_parameters
394
+ data
335
395
  end
336
396
 
337
397
  # Add query params in place of missing body params, if configured.
@@ -370,7 +430,7 @@ module RESTFramework::BaseModelControllerMixin
370
430
  return @recordset = nil
371
431
  end
372
432
 
373
- # Helper to get the records this controller has access to *after* any filtering is applied.
433
+ # Get the records this controller has access to *after* any filtering is applied.
374
434
  def get_records
375
435
  return @records if instance_variable_defined?(:@records)
376
436
 
@@ -403,7 +463,17 @@ module RESTFramework::BaseModelControllerMixin
403
463
  end
404
464
 
405
465
  # Return the record. Route key is always `:id` by Rails convention.
406
- return @record = recordset.find_by!(find_by_key => params[:id])
466
+ return @record = recordset.find_by!(find_by_key => request.path_parameters[:id])
467
+ end
468
+
469
+ # Create a transaction around the passed block, if configured. This is used primarily for bulk
470
+ # actions, but we include it here so it's always available.
471
+ def self._rrf_bulk_transaction(&block)
472
+ if self.bulk_transactional
473
+ ActiveRecord::Base.transaction(&block)
474
+ else
475
+ yield
476
+ end
407
477
  end
408
478
  end
409
479
 
@@ -413,7 +483,7 @@ module RESTFramework::ListModelMixin
413
483
  return api_response(self.get_index_records)
414
484
  end
415
485
 
416
- # Helper to get records with both filtering and pagination applied.
486
+ # Get records with both filtering and pagination applied.
417
487
  def get_index_records
418
488
  records = self.get_records
419
489
 
@@ -448,17 +518,19 @@ module RESTFramework::CreateModelMixin
448
518
  return api_response(self.create!, status: :created)
449
519
  end
450
520
 
451
- # Helper to perform the `create!` call and return the created record.
521
+ # Perform the `create!` call and return the created record.
452
522
  def create!
453
- if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
523
+ create_from = if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
454
524
  # Create with any properties inherited from the recordset. We exclude any `select` clauses in
455
525
  # case model callbacks need to call `count` on this collection, which typically raises a SQL
456
526
  # `SyntaxError`.
457
- return self.get_recordset.except(:select).create!(self.get_create_params)
527
+ self.get_recordset.except(:select)
458
528
  else
459
529
  # Otherwise, perform a "bare" create.
460
- return self.class.get_model.create!(self.get_create_params)
530
+ self.class.get_model
461
531
  end
532
+
533
+ return create_from.create!(self.get_create_params)
462
534
  end
463
535
  end
464
536
 
@@ -468,7 +540,7 @@ module RESTFramework::UpdateModelMixin
468
540
  return api_response(self.update!)
469
541
  end
470
542
 
471
- # Helper to perform the `update!` call and return the updated record.
543
+ # Perform the `update!` call and return the updated record.
472
544
  def update!
473
545
  record = self.get_record
474
546
  record.update!(self.get_update_params)
@@ -483,7 +555,7 @@ module RESTFramework::DestroyModelMixin
483
555
  return api_response("")
484
556
  end
485
557
 
486
- # Helper to perform the `destroy!` call and return the destroyed (and frozen) record.
558
+ # Perform the `destroy!` call and return the destroyed (and frozen) record.
487
559
  def destroy!
488
560
  return self.get_record.destroy!
489
561
  end
@@ -493,29 +565,25 @@ end
493
565
  module RESTFramework::ReadOnlyModelControllerMixin
494
566
  include RESTFramework::BaseModelControllerMixin
495
567
 
496
- def self.included(base)
497
- return unless base.is_a?(Class)
568
+ include RESTFramework::ListModelMixin
569
+ include RESTFramework::ShowModelMixin
498
570
 
571
+ def self.included(base)
499
572
  RESTFramework::BaseModelControllerMixin.included(base)
500
573
  end
501
-
502
- include RESTFramework::ListModelMixin
503
- include RESTFramework::ShowModelMixin
504
574
  end
505
575
 
506
576
  # Mixin that includes all the CRUD mixins.
507
577
  module RESTFramework::ModelControllerMixin
508
578
  include RESTFramework::BaseModelControllerMixin
509
579
 
510
- def self.included(base)
511
- return unless base.is_a?(Class)
512
-
513
- RESTFramework::BaseModelControllerMixin.included(base)
514
- end
515
-
516
580
  include RESTFramework::ListModelMixin
517
581
  include RESTFramework::ShowModelMixin
518
582
  include RESTFramework::CreateModelMixin
519
583
  include RESTFramework::UpdateModelMixin
520
584
  include RESTFramework::DestroyModelMixin
585
+
586
+ def self.included(base)
587
+ RESTFramework::BaseModelControllerMixin.included(base)
588
+ end
521
589
  end
@@ -2,4 +2,5 @@ module RESTFramework::ControllerMixins
2
2
  end
3
3
 
4
4
  require_relative "controller_mixins/base"
5
+ require_relative "controller_mixins/bulk"
5
6
  require_relative "controller_mixins/models"
@@ -14,23 +14,59 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
14
14
  # Get a list of filterset fields for the current action. Fallback to columns because we don't want
15
15
  # to try filtering by any query parameter because that could clash with other query parameters.
16
16
  def _get_fields
17
- return @controller.class.filterset_fields || @controller.get_fields(fallback: true)
17
+ return @_get_fields ||= (
18
+ @controller.class.filterset_fields || @controller.get_fields(fallback: true)
19
+ ).map(&:to_s)
18
20
  end
19
21
 
20
22
  # Filter params for keys allowed by the current action's filterset_fields/fields config.
21
23
  def _get_filter_params
22
24
  # Map filterset fields to strings because query parameter keys are strings.
23
- if fields = self._get_fields.map(&:to_s)
24
- return @controller.request.query_parameters.select { |p, _| fields.include?(p) }
25
- end
25
+ fields = self._get_fields
26
+ @associations = []
27
+
28
+ return @controller.request.query_parameters.select { |p, _|
29
+ # Remove any trailing `__in` from the field name.
30
+ field = p.chomp("__in")
31
+
32
+ # Remove any associations whose sub-fields are not filterable. Also populate `@associations`
33
+ # so the caller can include them.
34
+ if match = /(.*)\.(.*)/.match(field)
35
+ field, sub_field = match[1..2]
36
+ next false unless field.in?(fields)
37
+
38
+ sub_fields = @controller.class.get_field_config(field)[:sub_fields]
39
+ if sub_field.in?(sub_fields)
40
+ @associations << field.to_sym
41
+ next true
42
+ end
43
+
44
+ next false
45
+ end
46
+
47
+ next field.in?(fields)
48
+ }.map { |p, v|
49
+ # Convert fields ending in `__in` to array values.
50
+ if p.end_with?("__in")
51
+ p = p.chomp("__in")
52
+ v = v.split(",")
53
+ end
54
+
55
+ # Convert "nil" and "null" to nil.
56
+ if v == "nil" || v == "null"
57
+ v = nil
58
+ end
26
59
 
27
- return @controller.request.query_parameters.to_h
60
+ [p, v]
61
+ }.to_h.symbolize_keys
28
62
  end
29
63
 
30
64
  # Filter data according to the request query parameters.
31
65
  def get_filtered_data(data)
32
- filter_params = self._get_filter_params.symbolize_keys
33
- unless filter_params.blank?
66
+ if filter_params = self._get_filter_params.presence
67
+ # Include any associations.
68
+ data = data.includes(*@associations) unless @associations.empty?
69
+
34
70
  return data.where(**filter_params)
35
71
  end
36
72
 
@@ -43,18 +79,22 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
43
79
  # Get a list of ordering fields for the current action. Do not fallback to columns in case the
44
80
  # user wants to order by a virtual column.
45
81
  def _get_fields
46
- return @controller.class.ordering_fields || @controller.get_fields
82
+ return @_get_fields ||= (
83
+ @controller.class.ordering_fields || @controller.get_fields
84
+ )&.map(&:to_s)
47
85
  end
48
86
 
49
87
  # Convert ordering string to an ordering configuration.
50
88
  def _get_ordering
51
89
  return nil if @controller.class.ordering_query_param.blank?
52
90
 
91
+ @associations = []
92
+
53
93
  # Ensure ordering_fields are strings since the split param will be strings.
54
- fields = self._get_fields&.map(&:to_s)
94
+ fields = self._get_fields
55
95
  order_string = @controller.params[@controller.class.ordering_query_param]
56
96
 
57
- if order_string.present? && fields
97
+ if order_string.present?
58
98
  ordering = {}.with_indifferent_access
59
99
  order_string.split(",").each do |field|
60
100
  if field[0] == "-"
@@ -64,9 +104,19 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
64
104
  column = field
65
105
  direction = :asc
66
106
  end
67
- if !fields || column.in?(fields)
68
- ordering[column] = direction
107
+ next unless !fields || column.in?(fields)
108
+
109
+ # Populate any `@associations` so the caller can include them.
110
+ if match = /(.*)\.(.*)/.match(column)
111
+ association, sub_field = match[1..2]
112
+ @associations << association.to_sym
113
+
114
+ # Also, due to Rails weirdness, we need to convert the association name to the table name.
115
+ table_name = @controller.class.get_model.reflections[association].table_name
116
+ column = "#{table_name}.#{sub_field}"
69
117
  end
118
+
119
+ ordering[column] = direction
70
120
  end
71
121
  return ordering
72
122
  end
@@ -80,7 +130,10 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
80
130
  reorder = !@controller.class.ordering_no_reorder
81
131
 
82
132
  if ordering && !ordering.empty?
83
- return data.send(reorder ? :reorder : :order, _get_ordering)
133
+ # Include any associations.
134
+ data = data.includes(*@associations) unless @associations.empty?
135
+
136
+ return data.send(reorder ? :reorder : :order, ordering)
84
137
  end
85
138
 
86
139
  return data
@@ -102,6 +102,15 @@ module ActionDispatch::Routing
102
102
  public_send(m, "", action: action) if self.respond_to?(m)
103
103
  end
104
104
  end
105
+
106
+ # Route bulk actions, if configured.
107
+ RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
108
+ next unless controller_class.method_defined?(action)
109
+
110
+ [methods].flatten.each do |m|
111
+ public_send(m, "", action: action) if self.respond_to?(m)
112
+ end
113
+ end
105
114
  end
106
115
 
107
116
  if unscoped
@@ -88,7 +88,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
88
88
  return self.config || self.singular_config || self.plural_config
89
89
  end
90
90
 
91
- # Helper to get a native serializer configuration from the controller.
91
+ # Get a native serializer configuration from the controller.
92
92
  def get_controller_native_serializer_config
93
93
  return nil unless @controller
94
94
 
@@ -101,9 +101,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
101
101
  return controller_serializer || @controller.class.try(:native_serializer_config)
102
102
  end
103
103
 
104
- # Helper to filter a single subconfig for specific keys. By default, keys from `fields` are
105
- # removed from the provided `subcfg`. There are two (mutually exclusive) options to adjust the
106
- # behavior:
104
+ # Filter a single subconfig for specific keys. By default, keys from `fields` are removed from the
105
+ # provided `subcfg`. There are two (mutually exclusive) options to adjust the behavior:
107
106
  #
108
107
  # `add`: Add any `fields` to the `subcfg` which aren't already in the `subcfg`.
109
108
  # `only`: Remove any values found in the `subcfg` not in `fields`.
@@ -149,16 +148,14 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
149
148
  return subcfg
150
149
  end
151
150
 
152
- # Helper to filter out configuration properties based on the :except query parameter.
153
- def filter_except(cfg)
151
+ # Filter out configuration properties based on the :except/:only query parameters.
152
+ def filter_from_request(cfg)
154
153
  return cfg unless @controller
155
154
 
156
155
  except_param = @controller.class.try(:native_serializer_except_query_param)
157
156
  only_param = @controller.class.try(:native_serializer_only_query_param)
158
157
  if except_param && except = @controller.request.query_parameters[except_param].presence
159
- except = except.split(",").map(&:strip).map(&:to_sym)
160
-
161
- unless except.empty?
158
+ if except = except.split(",").map(&:strip).map(&:to_sym).presence
162
159
  # Filter `only`, `except` (additive), `include`, `methods`, and `serializer_methods`.
163
160
  if cfg[:only]
164
161
  cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: except)
@@ -167,6 +164,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
167
164
  else
168
165
  cfg[:except] = except
169
166
  end
167
+
170
168
  cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: except)
171
169
  cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: except)
172
170
  cfg[:serializer_methods] = self.class.filter_subcfg(
@@ -174,20 +172,15 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
174
172
  )
175
173
  end
176
174
  elsif only_param && only = @controller.request.query_parameters[only_param].presence
177
- only = only.split(",").map(&:strip).map(&:to_sym)
178
-
179
- unless only.empty?
180
- # Filter `only`, `except` (additive), `include`, and `methods`.
175
+ if only = only.split(",").map(&:strip).map(&:to_sym).presence
176
+ # Filter `only`, `include`, and `methods`. Adding anything to `except` is not needed,
177
+ # because any configuration there takes precedence over `only`.
181
178
  if cfg[:only]
182
179
  cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: only, only: true)
183
- elsif cfg[:except]
184
- # For the `except` part of the serializer, we need to append any columns not in `only`.
185
- model = @controller.class.get_model
186
- except_cols = model&.column_names&.map(&:to_sym)&.reject { |c| c.in?(only) }
187
- cfg[:except] = self.class.filter_subcfg(cfg[:except], fields: except_cols, add: true)
188
180
  else
189
181
  cfg[:only] = only
190
182
  end
183
+
191
184
  cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: only, only: true)
192
185
  cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: only, only: true)
193
186
  cfg[:serializer_methods] = self.class.filter_subcfg(
@@ -213,18 +206,28 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
213
206
  end
214
207
 
215
208
  # If the config wasn't determined, build a serializer config from controller fields.
216
- if fields = @controller&.get_fields
209
+ if fields = @controller&.get_fields(fallback: true)
217
210
  fields = fields.deep_dup
218
211
 
219
212
  columns = []
220
- includes = []
213
+ includes = {}
221
214
  methods = []
222
215
  if @model
223
216
  fields.each do |f|
224
217
  if f.in?(@model.column_names)
225
218
  columns << f
226
219
  elsif @model.reflections.key?(f)
227
- includes << f
220
+ sub_columns = []
221
+ sub_methods = []
222
+ @controller.class.get_field_config(f)[:sub_fields].each do |sf|
223
+ sub_model = @model.reflections[f].klass
224
+ if sf.in?(sub_model.column_names)
225
+ sub_columns << sf
226
+ elsif sub_model.method_defined?(sf)
227
+ sub_methods << sf
228
+ end
229
+ end
230
+ includes[f] = {only: sub_columns, methods: sub_methods}
228
231
  elsif @model.method_defined?(f)
229
232
  methods << f
230
233
  end
@@ -242,10 +245,10 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
242
245
 
243
246
  # Get a configuration passable to `serializable_hash` for the object, filtered if required.
244
247
  def get_serializer_config
245
- return filter_except(self._get_raw_serializer_config)
248
+ return filter_from_request(self._get_raw_serializer_config)
246
249
  end
247
250
 
248
- # Internal helper to serialize a single record and merge results of `serializer_methods`.
251
+ # Serialize a single record and merge results of `serializer_methods`.
249
252
  def _serialize(record, config, serializer_methods)
250
253
  # Ensure serializer_methods is either falsy, or an array.
251
254
  if serializer_methods && !serializer_methods.respond_to?(:to_ary)
@@ -1,5 +1,6 @@
1
1
  module RESTFramework::Utils
2
2
  HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
3
+ LABEL_FIELDS = %w(name label login title email username)
3
4
 
4
5
  # Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`, and
5
6
  # additional metadata fields.
@@ -139,15 +140,63 @@ module RESTFramework::Utils
139
140
  end
140
141
 
141
142
  # Parse fields hashes.
142
- def self.parse_fields_hash(fields_hash, model)
143
- parsed_fields = fields_hash[:only] || model&.column_names || []
144
- parsed_fields -= fields_hash[:except] if fields_hash[:except]
143
+ def self.parse_fields_hash(fields_hash, model, exclude_associations: nil)
144
+ parsed_fields = fields_hash[:only] || (
145
+ model ? self.fields_for(model, exclude_associations: exclude_associations) : []
146
+ )
147
+ parsed_fields += fields_hash[:include] if fields_hash[:include]
148
+ parsed_fields -= fields_hash[:exclude] if fields_hash[:exclude]
145
149
 
146
150
  # Warn for any unknown keys.
147
- (fields_hash.keys - [:only, :except]).each do |k|
151
+ (fields_hash.keys - [:only, :include, :exclude]).each do |k|
148
152
  Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
149
153
  end
150
154
 
151
155
  return parsed_fields
152
156
  end
157
+
158
+ # Get the fields for a given model, including not just columns (which includes
159
+ # foreign keys), but also associations.
160
+ def self.fields_for(model, exclude_associations: nil)
161
+ foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
162
+
163
+ if exclude_associations
164
+ return model.column_names.reject { |c| c.in?(foreign_keys) }
165
+ end
166
+
167
+ # Add associations in addition to normal columns.
168
+ return model.column_names.reject { |c|
169
+ c.in?(foreign_keys)
170
+ } + model.reflections.map { |association, ref|
171
+ if ref.macro.in?([:has_many, :has_and_belongs_to_many]) &&
172
+ RESTFramework.config.large_reverse_association_tables&.include?(ref.table_name)
173
+ next nil
174
+ end
175
+
176
+ next association
177
+ }.compact
178
+ end
179
+
180
+ # Get the sub-fields that may be serialized and filtered/ordered for a reflection.
181
+ def self.sub_fields_for(ref)
182
+ model = ref.klass
183
+
184
+ if model
185
+ sub_fields = [model.primary_key].flatten.compact
186
+
187
+ # Preferrably find a database column to use as label.
188
+ if match = LABEL_FIELDS.find { |f| f.in?(model.column_names) }
189
+ return sub_fields + [match]
190
+ end
191
+
192
+ # Otherwise, find a method.
193
+ if match = LABEL_FIELDS.find { |f| model.method_defined?(f) }
194
+ return sub_fields + [match]
195
+ end
196
+
197
+ return sub_fields
198
+ end
199
+
200
+ return ["id", "name"]
201
+ end
153
202
  end
@@ -13,6 +13,10 @@ module RESTFramework
13
13
  RRF_BUILTIN_ACTIONS = {
14
14
  options: :options,
15
15
  }.freeze
16
+ RRF_BUILTIN_BULK_ACTIONS = {
17
+ update_all: [:put, :patch].freeze,
18
+ destroy_all: :delete,
19
+ }.freeze
16
20
 
17
21
  # Global configuration should be kept minimal, as controller-level configurations allows multiple
18
22
  # APIs to be defined to behave differently.
@@ -24,11 +28,25 @@ module RESTFramework
24
28
  # in:
25
29
  # - Model delegation, for the helper methods to be defined dynamically.
26
30
  # - Websockets, for `::Channel` class to be defined dynamically.
27
- # - Controller configuration finalization.
31
+ # - Controller configuration freezing.
28
32
  attr_accessor :disable_auto_finalize
29
33
 
30
34
  # Freeze configuration attributes during finalization to prevent accidental mutation.
31
35
  attr_accessor :freeze_config
36
+
37
+ # Specify reverse association tables that are typically very large, andd therefore should not be
38
+ # added to fields by default.
39
+ attr_accessor :large_reverse_association_tables
40
+
41
+ # Whether the backtrace should be shown in rescued errors.
42
+ attr_accessor :show_backtrace
43
+
44
+ # Option to disable `rescue_from` on the controller mixins.
45
+ attr_accessor :disable_rescue_from
46
+
47
+ def initialize
48
+ self.show_backtrace = Rails.env.development?
49
+ end
32
50
  end
33
51
 
34
52
  def self.config
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rest_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.4
4
+ version: 0.7.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Gregory N. Schmit
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-10 00:00:00.000000000 Z
11
+ date: 2023-01-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -42,6 +42,7 @@ files:
42
42
  - lib/rest_framework.rb
43
43
  - lib/rest_framework/controller_mixins.rb
44
44
  - lib/rest_framework/controller_mixins/base.rb
45
+ - lib/rest_framework/controller_mixins/bulk.rb
45
46
  - lib/rest_framework/controller_mixins/models.rb
46
47
  - lib/rest_framework/engine.rb
47
48
  - lib/rest_framework/errors.rb