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