rest_framework 1.1.0 → 2.0.0.beta1
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/README.md +107 -41
- data/VERSION +1 -1
- data/app/views/rest_framework/routes_and_forms/_html_form.html.erb +1 -1
- data/lib/rest_framework/controller/actions.rb +256 -0
- data/lib/rest_framework/controller/bulk.rb +247 -32
- data/lib/rest_framework/controller/crud.rb +13 -9
- data/lib/rest_framework/controller/openapi.rb +12 -8
- data/lib/rest_framework/controller.rb +275 -164
- data/lib/rest_framework/errors.rb +70 -3
- data/lib/rest_framework/filters/base_filter.rb +10 -0
- data/lib/rest_framework/filters/ordering_filter.rb +41 -23
- data/lib/rest_framework/filters/query_filter.rb +24 -5
- data/lib/rest_framework/filters/search_filter.rb +8 -4
- data/lib/rest_framework/paginators/page_number_paginator.rb +34 -19
- data/lib/rest_framework/routers.rb +52 -182
- data/lib/rest_framework/serializers/active_model_serializer_adapter_factory.rb +2 -2
- data/lib/rest_framework/serializers/base_serializer.rb +2 -2
- data/lib/rest_framework/serializers/native_serializer.rb +78 -24
- data/lib/rest_framework/utils.rb +39 -70
- data/lib/rest_framework/version.rb +8 -5
- data/lib/rest_framework.rb +7 -41
- metadata +5 -12
- data/lib/rest_framework/errors/base_error.rb +0 -5
- data/lib/rest_framework/errors/nil_passed_to_render_api_error.rb +0 -14
- data/lib/rest_framework/generators/controller_generator.rb +0 -64
- data/lib/rest_framework/generators.rb +0 -4
- data/lib/rest_framework/mixins/base_controller_mixin.rb +0 -12
- data/lib/rest_framework/mixins/bulk_model_controller_mixin.rb +0 -55
- data/lib/rest_framework/mixins/model_controller_mixin.rb +0 -110
- data/lib/rest_framework/mixins.rb +0 -7
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
module RESTFramework::Controller
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
RRF_DEFAULT_BULK_MAX_SIZE = 1000
|
|
3
|
+
RRF_DEFAULT_BULK_MAX_RAW_SIZE = 10000
|
|
4
|
+
|
|
5
|
+
def _bulk_max_size
|
|
6
|
+
@_bulk_max_size ||= self.class.bulk_max_size || RRF_DEFAULT_BULK_MAX_SIZE
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def _bulk_max_raw_size
|
|
10
|
+
@_bulk_max_raw_size ||= self.class.bulk_max_raw_size || RRF_DEFAULT_BULK_MAX_RAW_SIZE
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def _bulk_serialize(records)
|
|
5
14
|
# This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
|
|
6
15
|
# the serializer directly. This would fail for active model serializers, but maybe we don't
|
|
7
16
|
# care?
|
|
@@ -11,52 +20,258 @@ module RESTFramework::Controller
|
|
|
11
20
|
end
|
|
12
21
|
end
|
|
13
22
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
create_data = self.get_create_params(bulk_mode: true)[:_json]
|
|
23
|
+
def _bulk_mode
|
|
24
|
+
return @_bulk_mode if defined?(@_bulk_mode)
|
|
17
25
|
|
|
18
|
-
#
|
|
19
|
-
|
|
26
|
+
# If mode override is allowed, check the query param.
|
|
27
|
+
if self.class.bulk_allow_mode_override && (qp = self.class.bulk_mode_query_param)
|
|
28
|
+
if (requested = request.query_parameters[qp].presence)
|
|
29
|
+
requested = requested.to_sym
|
|
30
|
+
unless requested.in?([ :default, :raw ])
|
|
31
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
32
|
+
"Invalid bulk mode: #{requested}. Must be `default` or `raw`.",
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
return @_bulk_mode = requested
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Normalize: `true` and `:default` both mean per-record processing.
|
|
40
|
+
@_bulk_mode = self.class.bulk == :raw ? :raw : :default
|
|
20
41
|
end
|
|
21
42
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
43
|
+
# Resolve whether partial fulfillment is enabled for this request.
|
|
44
|
+
def _bulk_partial
|
|
45
|
+
return @_bulk_partial if defined?(@_bulk_partial)
|
|
46
|
+
|
|
47
|
+
# Check the query param first if configured.
|
|
48
|
+
if (qp = self.class.bulk_partial_query_param)
|
|
49
|
+
if (requested = request.query_parameters[qp].presence)
|
|
50
|
+
return @_bulk_partial = ActiveModel::Type::Boolean.new.cast(requested)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
@_bulk_partial = self.class.bulk_partial
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Validate and extract bulk object data from request parameters.
|
|
58
|
+
def _bulk_object_data(bulk_action, bulk_mode)
|
|
59
|
+
data = self.get_body_params(bulk_action: bulk_action)[:_json]
|
|
60
|
+
|
|
61
|
+
unless data&.is_a?(Array) && data.all? { |r| r.is_a?(ActionController::Parameters) }
|
|
62
|
+
raise RESTFramework::InvalidBulkParametersError.new("Expected an array of objects.")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Enforce size limits.
|
|
66
|
+
max = bulk_mode == :raw ? self._bulk_max_raw_size : self._bulk_max_size
|
|
67
|
+
if max && data.length > max
|
|
68
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
69
|
+
"Too many records (#{data.length}) for #{bulk_mode} mode; maximum is #{max}.",
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
data
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Validate and extract bulk primary key data from request parameters.
|
|
77
|
+
def _bulk_pk_data
|
|
78
|
+
data = self.get_destroy_params(bulk_action: :destroy)[:_json]
|
|
79
|
+
|
|
80
|
+
unless data&.is_a?(Array) && data.all? { |r| r.is_a?(String) || r.is_a?(Numeric) }
|
|
81
|
+
raise RESTFramework::InvalidBulkParametersError.new("Expected an array of primary keys.")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Enforce size limits.
|
|
85
|
+
max = self._bulk_mode == :raw ? self._bulk_max_raw_size : self._bulk_max_size
|
|
86
|
+
if max && data.length > max
|
|
87
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
88
|
+
"Too many records (#{data.length}) for #{self._bulk_mode} mode; maximum is #{max}.",
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
data
|
|
26
93
|
end
|
|
27
94
|
|
|
28
|
-
|
|
29
|
-
|
|
95
|
+
def create_all
|
|
96
|
+
if self._bulk_mode == :raw
|
|
97
|
+
result = self.create_all_raw!
|
|
98
|
+
return render_api({ message: "Bulk create successful.", result: result })
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
records = self.create_all_default!
|
|
102
|
+
render_api(
|
|
103
|
+
{ message: "Bulk create successful.", records: self._bulk_serialize(records) },
|
|
104
|
+
status: :created,
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def create_all_raw!
|
|
30
109
|
pk = self.class.model.primary_key
|
|
31
|
-
data =
|
|
32
|
-
|
|
110
|
+
data = self._bulk_object_data(:create, :raw)
|
|
111
|
+
|
|
112
|
+
unless first_keys = data.first&.keys&.sort
|
|
113
|
+
raise RESTFramework::InvalidBulkParametersError.new("Expected objects with attrs.")
|
|
114
|
+
end
|
|
115
|
+
unless data.all? { |r| r.keys.sort == first_keys }
|
|
116
|
+
raise RESTFramework::InvalidBulkParametersError.new("All objects must have the same attrs.")
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
self.create_from.insert_all(data, unique_by: pk)
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def create_all_default!
|
|
123
|
+
data = self._bulk_object_data(:create, :default)
|
|
124
|
+
collection = self.create_from
|
|
125
|
+
|
|
126
|
+
if self._bulk_partial
|
|
127
|
+
# Partial: save each record individually, return all (some may have errors).
|
|
128
|
+
data.map { |attrs| collection.create(attrs) }
|
|
33
129
|
else
|
|
34
|
-
|
|
35
|
-
{
|
|
130
|
+
# Transactional: validate all first, then save in a transaction or raise.
|
|
131
|
+
records = data.map { |attrs| collection.new(attrs) }
|
|
132
|
+
failed = records.reject(&:valid?)
|
|
133
|
+
|
|
134
|
+
if failed.any?
|
|
135
|
+
raise RESTFramework::BulkRecordErrorsError.new(records)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
self.class.model.transaction do
|
|
139
|
+
records.each(&:save!)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
records
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def update_all
|
|
147
|
+
if self._bulk_mode == :raw
|
|
148
|
+
result = self.update_all_raw!
|
|
149
|
+
return render_api({ message: "Bulk update successful.", result: result })
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
records = self.update_all_default!
|
|
153
|
+
render_api({ message: "Bulk update successful.", records: self._bulk_serialize(records) })
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def update_all_raw!
|
|
157
|
+
pk = self.class.model.primary_key
|
|
158
|
+
pk_type = self.class.model.type_for_attribute(pk)
|
|
159
|
+
data = self._bulk_object_data(:update, :raw)
|
|
160
|
+
|
|
161
|
+
# Cast ids like `update_all_default!` so the existence check compares matching types.
|
|
162
|
+
data_ids = data.map { |r| pk_type.cast(r[pk]) }.uniq
|
|
163
|
+
if data_ids.include?(nil)
|
|
164
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
165
|
+
"Bulk update requires the primary key (#{pk}) for all records.",
|
|
166
|
+
)
|
|
167
|
+
end
|
|
168
|
+
found_ids = self.get_recordset.where(pk => data_ids).pluck(pk)
|
|
169
|
+
if found_ids.length != data_ids.length
|
|
170
|
+
missing = data_ids - found_ids
|
|
171
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
172
|
+
"Records not found with #{pk}: #{missing.join(', ')}.",
|
|
173
|
+
)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
unless first_keys = data.first&.keys&.sort
|
|
177
|
+
raise RESTFramework::InvalidBulkParametersError.new("Expected objects with attrs.")
|
|
178
|
+
end
|
|
179
|
+
unless data.all? { |r| r.keys.sort == first_keys }
|
|
180
|
+
raise RESTFramework::InvalidBulkParametersError.new("All objects must have the same attrs.")
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
self.get_recordset.upsert_all(data, unique_by: pk)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def update_all_default!
|
|
187
|
+
pk = self.class.model.primary_key
|
|
188
|
+
pk_type = self.class.model.type_for_attribute(pk)
|
|
189
|
+
data = self._bulk_object_data(:update, :default)
|
|
190
|
+
|
|
191
|
+
# Cast ids to the pk's type so they match records fetched from the DB; JSON clients often send
|
|
192
|
+
# ids as strings, which otherwise never match the type-cast keys of `existing` below.
|
|
193
|
+
data_ids = data.map { |r| pk_type.cast(r[pk]) }.uniq
|
|
194
|
+
if data_ids.include?(nil)
|
|
195
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
196
|
+
"Bulk update requires the primary key (#{pk}) for all records.",
|
|
197
|
+
)
|
|
36
198
|
end
|
|
199
|
+
existing = self.get_recordset.where(pk => data_ids).index_by { |r| r.send(pk) }
|
|
200
|
+
if existing.length != data_ids.length
|
|
201
|
+
missing = data_ids - existing.keys
|
|
202
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
203
|
+
"Records not found with #{pk}: #{missing.join(', ')}.",
|
|
204
|
+
)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Assign attributes to each record.
|
|
208
|
+
records = data.map { |attrs|
|
|
209
|
+
record = existing[pk_type.cast(attrs[pk])]
|
|
210
|
+
record.assign_attributes(attrs.except(pk))
|
|
211
|
+
record
|
|
212
|
+
}
|
|
37
213
|
|
|
38
|
-
|
|
39
|
-
|
|
214
|
+
if self._bulk_partial
|
|
215
|
+
# Partial: save each record individually.
|
|
216
|
+
records.each(&:save)
|
|
217
|
+
records
|
|
218
|
+
else
|
|
219
|
+
# Transactional: validate all first, then save in a transaction or raise.
|
|
220
|
+
failed = records.reject(&:valid?)
|
|
221
|
+
|
|
222
|
+
if failed.any?
|
|
223
|
+
raise RESTFramework::BulkRecordErrorsError.new(records)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
self.class.model.transaction do
|
|
227
|
+
records.each(&:save!)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
records
|
|
231
|
+
end
|
|
40
232
|
end
|
|
41
233
|
|
|
42
234
|
def destroy_all
|
|
43
|
-
if
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
return render(api: serialized_records)
|
|
235
|
+
if self._bulk_mode == :raw
|
|
236
|
+
deleted = self.destroy_all_raw!
|
|
237
|
+
return render_api({ message: "Bulk destroy successful.", result: deleted })
|
|
47
238
|
end
|
|
48
239
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
)
|
|
240
|
+
records = self.destroy_all_default!
|
|
241
|
+
render_api({ message: "Bulk destroy successful.", records: self._bulk_serialize(records) })
|
|
52
242
|
end
|
|
53
243
|
|
|
54
|
-
|
|
55
|
-
|
|
244
|
+
def destroy_all_raw!
|
|
245
|
+
data = self._bulk_pk_data
|
|
56
246
|
pk = self.class.model.primary_key
|
|
57
|
-
|
|
247
|
+
self.get_recordset.where(pk => data).delete_all
|
|
248
|
+
end
|
|
58
249
|
|
|
59
|
-
|
|
60
|
-
|
|
250
|
+
def destroy_all_default!
|
|
251
|
+
data = self._bulk_pk_data
|
|
252
|
+
pk = self.class.model.primary_key
|
|
253
|
+
records = self.get_recordset.where(pk => data).to_a
|
|
254
|
+
|
|
255
|
+
# In transactional mode, verify all requested records exist.
|
|
256
|
+
if !self._bulk_partial && records.length != data.uniq.length
|
|
257
|
+
found_ids = records.map { |r| r.send(pk) }
|
|
258
|
+
missing = data.uniq - found_ids
|
|
259
|
+
raise RESTFramework::InvalidBulkParametersError.new(
|
|
260
|
+
"Bulk destroy requires all records to exist. Missing #{pk}: #{missing.join(', ')}.",
|
|
261
|
+
)
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
if self._bulk_partial
|
|
265
|
+
# Partial: destroy each record individually.
|
|
266
|
+
records.each(&:destroy)
|
|
267
|
+
records
|
|
268
|
+
else
|
|
269
|
+
# Transactional: destroy all in a transaction, roll back on failure.
|
|
270
|
+
self.class.model.transaction do
|
|
271
|
+
records.each(&:destroy!)
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
records
|
|
275
|
+
end
|
|
61
276
|
end
|
|
62
277
|
end
|
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
module RESTFramework::Controller
|
|
2
2
|
def create
|
|
3
|
-
# Bulk create: if `bulk` is enabled and the request body is an array, delegate to `create_all
|
|
3
|
+
# Bulk create: if `bulk` is enabled and the request body is an array, delegate to `create_all`.
|
|
4
4
|
if self.class.bulk && params[:_json].is_a?(Array)
|
|
5
|
-
|
|
6
|
-
return render(api: self.bulk_serialize(records))
|
|
5
|
+
return self.create_all
|
|
7
6
|
end
|
|
8
7
|
|
|
9
|
-
|
|
8
|
+
render_api(self.create!, status: :created)
|
|
10
9
|
end
|
|
11
10
|
|
|
12
11
|
# Perform the `create!` call and return the created record.
|
|
@@ -15,11 +14,11 @@ module RESTFramework::Controller
|
|
|
15
14
|
end
|
|
16
15
|
|
|
17
16
|
def index
|
|
18
|
-
|
|
17
|
+
render_api(self.class.model ? self.index! : self.index_content)
|
|
19
18
|
end
|
|
20
19
|
|
|
21
20
|
# Get records with both filtering and pagination applied.
|
|
22
|
-
def
|
|
21
|
+
def index!
|
|
23
22
|
records = self.get_records
|
|
24
23
|
|
|
25
24
|
# Handle pagination, if enabled.
|
|
@@ -39,12 +38,17 @@ module RESTFramework::Controller
|
|
|
39
38
|
records
|
|
40
39
|
end
|
|
41
40
|
|
|
41
|
+
# The payload rendered at a non-model controller's index (e.g. an API root). Override this.
|
|
42
|
+
def index_content
|
|
43
|
+
{ message: self.class.description.presence || "This is the API root." }
|
|
44
|
+
end
|
|
45
|
+
|
|
42
46
|
def show
|
|
43
|
-
|
|
47
|
+
render_api(self.get_record)
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
def update
|
|
47
|
-
|
|
51
|
+
render_api(self.update!)
|
|
48
52
|
end
|
|
49
53
|
|
|
50
54
|
# Perform the `update!` call and return the updated record.
|
|
@@ -56,7 +60,7 @@ module RESTFramework::Controller
|
|
|
56
60
|
|
|
57
61
|
def destroy
|
|
58
62
|
self.destroy!
|
|
59
|
-
|
|
63
|
+
render_api("")
|
|
60
64
|
end
|
|
61
65
|
|
|
62
66
|
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
|
@@ -180,6 +180,7 @@ module RESTFramework::Controller
|
|
|
180
180
|
end
|
|
181
181
|
|
|
182
182
|
v[:readOnly] = true if cfg[:read_only]
|
|
183
|
+
v[:writeOnly] = true if cfg[:write_only]
|
|
183
184
|
v[:default] = cfg[:default] if cfg.key?(:default)
|
|
184
185
|
|
|
185
186
|
if enum_variants = cfg[:enum_variants]
|
|
@@ -194,17 +195,20 @@ module RESTFramework::Controller
|
|
|
194
195
|
v[:"x-rrf-kind"] = cfg[:kind] if cfg[:kind]
|
|
195
196
|
|
|
196
197
|
if cfg[:reflection]
|
|
198
|
+
ref = cfg[:reflection]
|
|
197
199
|
v[:"x-rrf-reflection"] = {
|
|
198
|
-
class_name:
|
|
199
|
-
foreign_key:
|
|
200
|
-
association_foreign_key:
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
200
|
+
class_name: ref.respond_to?(:class_name) ? ref.class_name : nil,
|
|
201
|
+
foreign_key: ref.respond_to?(:foreign_key) ? ref.foreign_key : nil,
|
|
202
|
+
association_foreign_key: ref.respond_to?(:association_foreign_key) ?
|
|
203
|
+
ref.association_foreign_key : nil,
|
|
204
|
+
association_primary_key: ref.respond_to?(:association_primary_key) ?
|
|
205
|
+
ref.association_primary_key : nil,
|
|
206
|
+
inverse_of: ref.respond_to?(:inverse_of) ? ref.inverse_of&.name : nil,
|
|
207
|
+
join_table: ref.respond_to?(:join_table) ? ref.join_table : nil,
|
|
204
208
|
}.compact
|
|
205
209
|
v[:"x-rrf-association_pk"] = cfg[:association_pk]
|
|
206
|
-
v[:"x-rrf-
|
|
207
|
-
v[:"x-rrf-
|
|
210
|
+
v[:"x-rrf-association_fields"] = cfg[:fields]
|
|
211
|
+
v[:"x-rrf-association_fields_metadata"] = cfg[:association_fields_metadata]
|
|
208
212
|
v[:"x-rrf-id_field"] = cfg[:id_field]
|
|
209
213
|
v[:"x-rrf-nested_attributes_options"] = cfg[:nested_attributes_options]
|
|
210
214
|
end
|