rest_framework 0.7.5 → 0.7.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/lib/rest_framework/controller_mixins/base.rb +25 -46
- data/lib/rest_framework/controller_mixins/bulk.rb +34 -3
- data/lib/rest_framework/controller_mixins/models.rb +125 -61
- data/lib/rest_framework/filters.rb +66 -13
- data/lib/rest_framework/serializers.rb +22 -18
- data/lib/rest_framework/utils.rb +53 -4
- data/lib/rest_framework.rb +15 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: edeb7963dbca0185ff384e628a185c83341e3154d6414c9d8f70a05115abd23a
|
4
|
+
data.tar.gz: 8d6bc94fea07e57c20625c8aa192ced88789e078bf3a4b29b71e6c80099f2436
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 68ecf8a8a045c3e217c597f86b46573dc80bb931e20be45af01b939079f8cf2dd27d68e8bebd8d458726b04b8b55fb2dd65a60bcf4c847c30feb1257f7cfb9ea
|
7
|
+
data.tar.gz: 388478f7c8f2c51a96740f25043aa7d188b5e59731478f4bca2ce6171fadc5ba1e201b12ee051859386ce151c1114b9b6f207dbe840e732dec652d9639323825
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.7.
|
1
|
+
0.7.6
|
@@ -12,7 +12,6 @@ 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,
|
@@ -21,7 +20,7 @@ module RESTFramework::BaseControllerMixin
|
|
21
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,
|
@@ -170,11 +169,17 @@ module RESTFramework::BaseControllerMixin
|
|
170
169
|
end
|
171
170
|
|
172
171
|
# Handle some common exceptions.
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
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
|
178
183
|
|
179
184
|
# Use `TracePoint` hook to automatically call `rrf_finalize`.
|
180
185
|
unless RESTFramework.config.disable_auto_finalize
|
@@ -222,6 +227,7 @@ module RESTFramework::BaseControllerMixin
|
|
222
227
|
|
223
228
|
# Filter an arbitrary data set over all configured filter backends.
|
224
229
|
def get_filtered_data(data)
|
230
|
+
# Apply each filter sequentially.
|
225
231
|
self.get_filter_backends.each do |filter_class|
|
226
232
|
filter = filter_class.new(controller: self)
|
227
233
|
data = filter.get_filtered_data(data)
|
@@ -234,48 +240,21 @@ module RESTFramework::BaseControllerMixin
|
|
234
240
|
return self.class.get_options_metadata
|
235
241
|
end
|
236
242
|
|
237
|
-
def
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
end
|
245
|
-
|
246
|
-
def record_not_found(e)
|
247
|
-
return api_response(
|
248
|
-
{
|
249
|
-
message: "Record not found.",
|
250
|
-
}.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
|
251
|
-
status: 404,
|
252
|
-
)
|
253
|
-
end
|
254
|
-
|
255
|
-
def record_not_saved(e)
|
256
|
-
return api_response(
|
257
|
-
{
|
258
|
-
message: "Record not saved.", errors: e.record&.errors
|
259
|
-
}.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
|
260
|
-
status: 400,
|
261
|
-
)
|
262
|
-
end
|
263
|
-
|
264
|
-
def record_not_destroyed(e)
|
265
|
-
return api_response(
|
266
|
-
{
|
267
|
-
message: "Record not destroyed.", errors: e.record&.errors
|
268
|
-
}.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
|
269
|
-
status: 400,
|
270
|
-
)
|
271
|
-
end
|
243
|
+
def rrf_error_handler(e)
|
244
|
+
status = case e
|
245
|
+
when ActiveRecord::RecordNotFound
|
246
|
+
404
|
247
|
+
else
|
248
|
+
400
|
249
|
+
end
|
272
250
|
|
273
|
-
def unknown_attribute_error(e)
|
274
251
|
return api_response(
|
275
252
|
{
|
276
|
-
message: e.message
|
277
|
-
|
278
|
-
|
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,
|
279
258
|
)
|
280
259
|
end
|
281
260
|
|
@@ -2,14 +2,44 @@ require_relative "models"
|
|
2
2
|
|
3
3
|
# Mixin for creating records in bulk. This is unique compared to update/destroy because we overload
|
4
4
|
# the existing `create` action to support bulk creation.
|
5
|
+
# :nocov:
|
5
6
|
module RESTFramework::BulkCreateModelMixin
|
6
7
|
def create
|
7
|
-
|
8
|
+
status, payload = self.create_all!
|
9
|
+
return api_response(payload, status: status)
|
8
10
|
end
|
9
11
|
|
10
|
-
# Perform the `create
|
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`.
|
11
16
|
def create_all!
|
12
|
-
|
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
|
13
43
|
end
|
14
44
|
end
|
15
45
|
|
@@ -49,3 +79,4 @@ module RESTFramework::BulkModelControllerMixin
|
|
49
79
|
RESTFramework::ModelControllerMixin.included(base)
|
50
80
|
end
|
51
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
|
-
#
|
42
|
-
create_from_recordset: true,
|
43
|
-
|
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
|
135
|
+
def get_fields_metadata
|
82
136
|
# Get metadata sources.
|
83
137
|
model = self.get_model
|
84
|
-
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
|
-
#
|
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"
|
126
181
|
|
127
|
-
|
128
|
-
|
182
|
+
# Get any type information from the attribute.
|
183
|
+
if type = attribute.type
|
184
|
+
metadata[:type] ||= type.type
|
185
|
+
|
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
|
-
#
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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,37 +231,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
156
231
|
metadata[:validators][kind] << options
|
157
232
|
end
|
158
233
|
|
159
|
-
|
160
|
-
|
161
|
-
end
|
162
|
-
|
163
|
-
# Get metadata about the resource's associations (reflections).
|
164
|
-
def get_associations_metadata
|
165
|
-
return self.get_model.reflections.map { |k, v|
|
166
|
-
begin
|
167
|
-
pk = v.active_record_primary_key
|
168
|
-
rescue ActiveRecord::UnknownPrimaryKey
|
169
|
-
end
|
234
|
+
# Serialize any field config.
|
235
|
+
metadata[:config] = self.get_field_config(f).presence
|
170
236
|
|
171
|
-
next [
|
172
|
-
macro: v.macro,
|
173
|
-
label: self.get_label(k),
|
174
|
-
class_name: v.class_name,
|
175
|
-
foreign_key: v.foreign_key,
|
176
|
-
primary_key: pk,
|
177
|
-
polymorphic: v.polymorphic?,
|
178
|
-
table_name: v.table_name,
|
179
|
-
options: v.options.presence,
|
180
|
-
}.compact]
|
237
|
+
next [f, metadata.compact]
|
181
238
|
}.to_h
|
182
239
|
end
|
183
240
|
|
184
241
|
# Get a hash of metadata to be rendered in the `OPTIONS` response. Cache the result.
|
185
|
-
def get_options_metadata
|
242
|
+
def get_options_metadata
|
186
243
|
return super().merge(
|
187
244
|
{
|
188
|
-
fields: self.get_fields_metadata
|
189
|
-
associations: self.get_associations_metadata,
|
245
|
+
fields: self.get_fields_metadata,
|
190
246
|
},
|
191
247
|
)
|
192
248
|
end
|
@@ -271,27 +327,23 @@ module RESTFramework::BaseModelControllerMixin
|
|
271
327
|
return (action_config[action] if action) || self.class.send(generic_config_key)
|
272
328
|
end
|
273
329
|
|
274
|
-
# Get fields without any action context. Always fallback to columns at the class level.
|
275
|
-
def self.get_fields
|
276
|
-
if self.fields.is_a?(Hash)
|
277
|
-
return RESTFramework::Utils.parse_fields_hash(self.fields, self.get_model)
|
278
|
-
end
|
279
|
-
|
280
|
-
return self.fields || self.get_model&.column_names || []
|
281
|
-
end
|
282
|
-
|
283
330
|
# Get a list of fields for the current action. Returning `nil` indicates that anything should be
|
284
331
|
# accepted unless `fallback` is true, in which case we should fallback to this controller's model
|
285
332
|
# columns, or en empty array.
|
286
333
|
def get_fields(fallback: false)
|
287
334
|
fields = _get_specific_action_config(:action_fields, :fields)
|
288
335
|
|
289
|
-
# If fields is a hash, then parse
|
336
|
+
# If fields is a hash, then parse it.
|
290
337
|
if fields.is_a?(Hash)
|
291
|
-
return RESTFramework::Utils.parse_fields_hash(
|
338
|
+
return RESTFramework::Utils.parse_fields_hash(
|
339
|
+
fields, self.class.get_model, exclude_associations: self.class.exclude_associations
|
340
|
+
)
|
292
341
|
elsif !fields && fallback
|
293
342
|
# Otherwise, if fields is nil and fallback is true, then fallback to columns.
|
294
|
-
|
343
|
+
model = self.class.get_model
|
344
|
+
return model ? RESTFramework::Utils.fields_for(
|
345
|
+
model, exclude_associations: self.class.exclude_associations
|
346
|
+
) : []
|
295
347
|
end
|
296
348
|
|
297
349
|
return fields
|
@@ -299,7 +351,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
299
351
|
|
300
352
|
# Pass fields to get dynamic metadata based on which fields are available.
|
301
353
|
def get_options_metadata
|
302
|
-
return self.class.get_options_metadata
|
354
|
+
return self.class.get_options_metadata
|
303
355
|
end
|
304
356
|
|
305
357
|
# Get a list of find_by fields for the current action. Do not fallback to columns in case the user
|
@@ -330,16 +382,16 @@ module RESTFramework::BaseModelControllerMixin
|
|
330
382
|
end
|
331
383
|
|
332
384
|
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
333
|
-
def get_body_params(
|
334
|
-
|
385
|
+
def get_body_params(data: nil)
|
386
|
+
data ||= request.request_parameters
|
335
387
|
|
336
388
|
# Filter the request body and map to strings. Return all params if we cannot resolve a list of
|
337
389
|
# allowed parameters or fields.
|
338
390
|
allowed_params = self.get_allowed_parameters&.map(&:to_s)
|
339
391
|
body_params = if allowed_params
|
340
|
-
|
392
|
+
data.select { |p| allowed_params.include?(p) }
|
341
393
|
else
|
342
|
-
|
394
|
+
data
|
343
395
|
end
|
344
396
|
|
345
397
|
# Add query params in place of missing body params, if configured.
|
@@ -411,7 +463,17 @@ module RESTFramework::BaseModelControllerMixin
|
|
411
463
|
end
|
412
464
|
|
413
465
|
# Return the record. Route key is always `:id` by Rails convention.
|
414
|
-
return @record = recordset.find_by!(find_by_key =>
|
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
|
415
477
|
end
|
416
478
|
end
|
417
479
|
|
@@ -458,15 +520,17 @@ module RESTFramework::CreateModelMixin
|
|
458
520
|
|
459
521
|
# Perform the `create!` call and return the created record.
|
460
522
|
def create!
|
461
|
-
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
|
462
524
|
# Create with any properties inherited from the recordset. We exclude any `select` clauses in
|
463
525
|
# case model callbacks need to call `count` on this collection, which typically raises a SQL
|
464
526
|
# `SyntaxError`.
|
465
|
-
|
527
|
+
self.get_recordset.except(:select)
|
466
528
|
else
|
467
529
|
# Otherwise, perform a "bare" create.
|
468
|
-
|
530
|
+
self.class.get_model
|
469
531
|
end
|
532
|
+
|
533
|
+
return create_from.create!(self.get_create_params)
|
470
534
|
end
|
471
535
|
end
|
472
536
|
|
@@ -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 @
|
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
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
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.
|
33
|
-
|
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 @
|
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
|
94
|
+
fields = self._get_fields
|
55
95
|
order_string = @controller.params[@controller.class.ordering_query_param]
|
56
96
|
|
57
|
-
if order_string.present?
|
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
|
-
|
68
|
-
|
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
|
-
|
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
|
@@ -148,16 +148,14 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
148
148
|
return subcfg
|
149
149
|
end
|
150
150
|
|
151
|
-
# Filter out configuration properties based on the :except query
|
152
|
-
def
|
151
|
+
# Filter out configuration properties based on the :except/:only query parameters.
|
152
|
+
def filter_from_request(cfg)
|
153
153
|
return cfg unless @controller
|
154
154
|
|
155
155
|
except_param = @controller.class.try(:native_serializer_except_query_param)
|
156
156
|
only_param = @controller.class.try(:native_serializer_only_query_param)
|
157
157
|
if except_param && except = @controller.request.query_parameters[except_param].presence
|
158
|
-
except = except.split(",").map(&:strip).map(&:to_sym)
|
159
|
-
|
160
|
-
unless except.empty?
|
158
|
+
if except = except.split(",").map(&:strip).map(&:to_sym).presence
|
161
159
|
# Filter `only`, `except` (additive), `include`, `methods`, and `serializer_methods`.
|
162
160
|
if cfg[:only]
|
163
161
|
cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: except)
|
@@ -166,6 +164,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
166
164
|
else
|
167
165
|
cfg[:except] = except
|
168
166
|
end
|
167
|
+
|
169
168
|
cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: except)
|
170
169
|
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: except)
|
171
170
|
cfg[:serializer_methods] = self.class.filter_subcfg(
|
@@ -173,20 +172,15 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
173
172
|
)
|
174
173
|
end
|
175
174
|
elsif only_param && only = @controller.request.query_parameters[only_param].presence
|
176
|
-
only = only.split(",").map(&:strip).map(&:to_sym)
|
177
|
-
|
178
|
-
|
179
|
-
# 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`.
|
180
178
|
if cfg[:only]
|
181
179
|
cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: only, only: true)
|
182
|
-
elsif cfg[:except]
|
183
|
-
# For the `except` part of the serializer, we need to append any columns not in `only`.
|
184
|
-
model = @controller.class.get_model
|
185
|
-
except_cols = model&.column_names&.map(&:to_sym)&.reject { |c| c.in?(only) }
|
186
|
-
cfg[:except] = self.class.filter_subcfg(cfg[:except], fields: except_cols, add: true)
|
187
180
|
else
|
188
181
|
cfg[:only] = only
|
189
182
|
end
|
183
|
+
|
190
184
|
cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: only, only: true)
|
191
185
|
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: only, only: true)
|
192
186
|
cfg[:serializer_methods] = self.class.filter_subcfg(
|
@@ -212,18 +206,28 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
212
206
|
end
|
213
207
|
|
214
208
|
# If the config wasn't determined, build a serializer config from controller fields.
|
215
|
-
if fields = @controller&.get_fields
|
209
|
+
if fields = @controller&.get_fields(fallback: true)
|
216
210
|
fields = fields.deep_dup
|
217
211
|
|
218
212
|
columns = []
|
219
|
-
includes =
|
213
|
+
includes = {}
|
220
214
|
methods = []
|
221
215
|
if @model
|
222
216
|
fields.each do |f|
|
223
217
|
if f.in?(@model.column_names)
|
224
218
|
columns << f
|
225
219
|
elsif @model.reflections.key?(f)
|
226
|
-
|
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}
|
227
231
|
elsif @model.method_defined?(f)
|
228
232
|
methods << f
|
229
233
|
end
|
@@ -241,7 +245,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
241
245
|
|
242
246
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
243
247
|
def get_serializer_config
|
244
|
-
return
|
248
|
+
return filter_from_request(self._get_raw_serializer_config)
|
245
249
|
end
|
246
250
|
|
247
251
|
# Serialize a single record and merge results of `serializer_methods`.
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -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] ||
|
144
|
-
|
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, :
|
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
|
data/lib/rest_framework.rb
CHANGED
@@ -28,11 +28,25 @@ module RESTFramework
|
|
28
28
|
# in:
|
29
29
|
# - Model delegation, for the helper methods to be defined dynamically.
|
30
30
|
# - Websockets, for `::Channel` class to be defined dynamically.
|
31
|
-
# - Controller configuration
|
31
|
+
# - Controller configuration freezing.
|
32
32
|
attr_accessor :disable_auto_finalize
|
33
33
|
|
34
34
|
# Freeze configuration attributes during finalization to prevent accidental mutation.
|
35
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
|
36
50
|
end
|
37
51
|
|
38
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
|
+
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-
|
11
|
+
date: 2023-01-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|