rest_framework 0.9.0 → 0.9.1
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/app/views/layouts/rest_framework.html.erb +24 -18
- data/app/views/rest_framework/_head.html.erb +18 -13
- data/app/views/rest_framework/_html_form.html.erb +1 -1
- data/app/views/rest_framework/_raw_form.html.erb +6 -3
- data/lib/rest_framework/controller_mixins/bulk.rb +52 -37
- data/lib/rest_framework/controller_mixins/models.rb +47 -33
- 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: '036384f3209aba594f8af47997294c73c4ca95294601d1abf46ee31086610296'
|
4
|
+
data.tar.gz: 52424a145b6c959eed3818c23a3e21cd952cb8cb9dc0520506803c08ba5b63b3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 28d9e2ec67ae6ac76f9709a78f7818f96336b87956e447095e48af59386c4cf5375d2fbc9ff243bc5bb915d982a868849a32bda8f3b5406c6fd853e6d2245a58
|
7
|
+
data.tar.gz: f8618a040b3ea3cdf44017a2b7dbedd465546f6b073750925635124b97a11e15486883d0902a8259ac8f4d94f4c0a582f6687f1f8148f7332c76342a67c9ff11
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.9.
|
1
|
+
0.9.1
|
@@ -64,7 +64,7 @@
|
|
64
64
|
<div>
|
65
65
|
<h1 class="m-0"><%= (@header_text if defined? @header_text) || @title %></h1>
|
66
66
|
<div style="float: right">
|
67
|
-
<% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "DELETE" } %>
|
67
|
+
<% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "DELETE" && r[:action] == "destroy" } %>
|
68
68
|
<button type="button" class="btn btn-danger" onclick="rrfDelete(this)">DELETE</button>
|
69
69
|
<% end %>
|
70
70
|
<% if @route_groups.values[0].any? { |r| r[:matches_path] && r[:verb] == "OPTIONS" } %>
|
@@ -150,22 +150,28 @@
|
|
150
150
|
Routes
|
151
151
|
</a>
|
152
152
|
</li>
|
153
|
-
<% @
|
153
|
+
<% @_rrf_form_routes_raw = @route_groups.values[0].select { |r|
|
154
|
+
r[:matches_params] && (
|
155
|
+
r[:verb].in?(["POST", "PUT", "PATCH"]) ||
|
156
|
+
(r[:verb] == "DELETE" && r[:action] == "destroy_all")
|
157
|
+
)
|
158
|
+
} %>
|
159
|
+
<% @_rrf_form_routes_html = @route_groups.values[0].select { |r|
|
154
160
|
r[:matches_params] && r[:verb].in?(["POST", "PUT", "PATCH"])
|
155
161
|
} %>
|
156
|
-
<%
|
162
|
+
<% if @_rrf_form_routes_raw.present? %>
|
157
163
|
<li class="nav-item">
|
158
|
-
<a class="nav-link" href="#
|
164
|
+
<a class="nav-link" href="#tabRawForm" data-bs-toggle="tab" role="tab">
|
159
165
|
Raw Form
|
160
166
|
</a>
|
161
167
|
</li>
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
</
|
168
|
-
|
168
|
+
<% end %>
|
169
|
+
<% if @_rrf_form_routes_html.present? && @is_model_controller %>
|
170
|
+
<li class="nav-item">
|
171
|
+
<a class="nav-link" href="#tabHtmlForm" data-bs-toggle="tab" role="tab">
|
172
|
+
HTML Form
|
173
|
+
</a>
|
174
|
+
</li>
|
169
175
|
<% end %>
|
170
176
|
</ul>
|
171
177
|
</div>
|
@@ -173,15 +179,15 @@
|
|
173
179
|
<div class="tab-pane fade show active" id="tab-routes" role="tabpanel">
|
174
180
|
<%= render partial: "rest_framework/routes" %>
|
175
181
|
</div>
|
176
|
-
<%
|
177
|
-
<div class="tab-pane fade" id="
|
182
|
+
<% if @_rrf_form_routes_raw.present? %>
|
183
|
+
<div class="tab-pane fade" id="tabRawForm" role="tabpanel">
|
178
184
|
<%= render partial: "rest_framework/raw_form" %>
|
179
185
|
</div>
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
186
|
+
<% end %>
|
187
|
+
<% if @_rrf_form_routes_html.present? && @is_model_controller %>
|
188
|
+
<div class="tab-pane fade" id="tabHtmlForm" role="tabpanel">
|
189
|
+
<%= render partial: "rest_framework/html_form" %>
|
190
|
+
</div>
|
185
191
|
<% end %>
|
186
192
|
</div>
|
187
193
|
</div>
|
@@ -303,6 +303,24 @@ document.addEventListener("DOMContentLoaded", (event) => {
|
|
303
303
|
event.preventDefault()
|
304
304
|
})
|
305
305
|
})
|
306
|
+
|
307
|
+
// Check if `rawFilesFormWrapper` should be displayed when media type is changed.
|
308
|
+
const rawFormRouteSelect = document.getElementById("rawFormRoute")
|
309
|
+
const rawFormMediaTypeSelect = document.getElementById("rawFormMediaType")
|
310
|
+
const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
|
311
|
+
if (rawFilesFormWrapper) {
|
312
|
+
const rawFormFilesHandler = () => {
|
313
|
+
const selectedRouteOption = rawFormRouteSelect.options[rawFormRouteSelect.selectedIndex]
|
314
|
+
if (rawFormMediaTypeSelect.value === "multipart/form-data" && selectedRouteOption.dataset.supportsFiles) {
|
315
|
+
rawFilesFormWrapper.style.display = "block"
|
316
|
+
} else {
|
317
|
+
rawFilesFormWrapper.style.display = "none"
|
318
|
+
}
|
319
|
+
}
|
320
|
+
|
321
|
+
rawFormRouteSelect.addEventListener("change", rawFormFilesHandler)
|
322
|
+
rawFormMediaTypeSelect.addEventListener("change", rawFormFilesHandler)
|
323
|
+
}
|
306
324
|
})
|
307
325
|
|
308
326
|
// Convert plain-text links to anchor tag links.
|
@@ -391,17 +409,4 @@ function rrfAPICall(path, method, kwargs={}) {
|
|
391
409
|
.then((response) => response.text())
|
392
410
|
.then((body) => { rrfReplaceDocument(body) })
|
393
411
|
}
|
394
|
-
|
395
|
-
// Check if `rawFilesFormWrapper` should be displayed when media type is changed.
|
396
|
-
function rrfCheckRawFilesFormDisplay(el) {
|
397
|
-
const rawFilesFormWrapper = document.getElementById("rawFilesFormWrapper")
|
398
|
-
|
399
|
-
if (rawFilesFormWrapper) {
|
400
|
-
if (el.value === "multipart/form-data") {
|
401
|
-
rawFilesFormWrapper.style.display = "block"
|
402
|
-
} else {
|
403
|
-
rawFilesFormWrapper.style.display = "none"
|
404
|
-
}
|
405
|
-
}
|
406
|
-
}
|
407
412
|
</script>
|
@@ -2,7 +2,7 @@
|
|
2
2
|
<div class="mb-2">
|
3
3
|
<label class="form-label w-100">Route
|
4
4
|
<select class="form-control form-control-sm" id="htmlFormRoute">
|
5
|
-
<% @
|
5
|
+
<% @_rrf_form_routes_html.each do |route| %>
|
6
6
|
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
7
7
|
<option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></option>
|
8
8
|
<% end %>
|
@@ -2,9 +2,12 @@
|
|
2
2
|
<div class="mb-2">
|
3
3
|
<label class="form-label w-100">Route
|
4
4
|
<select class="form-control form-control-sm" id="rawFormRoute">
|
5
|
-
<% @
|
5
|
+
<% @_rrf_form_routes_raw.each do |route| %>
|
6
6
|
<% path = @route_props[:with_path_args].call(route[:route]) %>
|
7
|
-
<option
|
7
|
+
<option
|
8
|
+
value="<%= route[:verb] %>:<%= path %>"
|
9
|
+
data-supports-files="<%= !route[:action].in?(["update_all", "destroy", "destroy_all"]) ? "true" : "" %>"
|
10
|
+
><%= route[:verb] %> <%= route[:relative_path] %></option>
|
8
11
|
<% end %>
|
9
12
|
</select>
|
10
13
|
</label>
|
@@ -12,7 +15,7 @@
|
|
12
15
|
|
13
16
|
<div class="mb-2">
|
14
17
|
<label class="form-label w-100">Media Type
|
15
|
-
<select class="form-control form-control-sm" id="rawFormMediaType"
|
18
|
+
<select class="form-control form-control-sm" id="rawFormMediaType">
|
16
19
|
<% ["application/json", "application/x-www-form-urlencoded", "multipart/form-data"].each do |t| %>
|
17
20
|
<option value="<%= t %>"><%= t %></option>
|
18
21
|
<% end %>
|
@@ -2,43 +2,31 @@ 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:
|
6
5
|
module RESTFramework::BulkCreateModelMixin
|
6
|
+
# While bulk update/destroy are obvious because they create new router endpoints, bulk create
|
7
|
+
# overloads the existing collection `POST` endpoint, so we add a special key to the options
|
8
|
+
# metadata to indicate bulk create is supported.
|
9
|
+
def get_options_metadata
|
10
|
+
return super.merge({bulk_create: true})
|
11
|
+
end
|
12
|
+
|
7
13
|
def create
|
8
|
-
|
9
|
-
|
14
|
+
if params[:_json].is_a?(Array)
|
15
|
+
records = self.create_all!
|
16
|
+
serialized_records = self.bulk_serialize(records)
|
17
|
+
return api_response(serialized_records)
|
18
|
+
end
|
19
|
+
|
20
|
+
return super
|
10
21
|
end
|
11
22
|
|
12
|
-
# Perform the `create`
|
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`.
|
23
|
+
# Perform the `create` call, and return the collection of (possibly) created records.
|
16
24
|
def create_all!
|
17
|
-
|
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
|
25
|
+
create_data = self.get_create_params(bulk_mode: true)[:_json]
|
27
26
|
|
28
|
-
|
29
|
-
|
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
|
27
|
+
# Perform bulk create in a transaction.
|
28
|
+
return ActiveRecord::Base.transaction do
|
29
|
+
next self.get_create_from.create(create_data)
|
42
30
|
end
|
43
31
|
end
|
44
32
|
end
|
@@ -46,24 +34,52 @@ end
|
|
46
34
|
# Mixin for updating records in bulk.
|
47
35
|
module RESTFramework::BulkUpdateModelMixin
|
48
36
|
def update_all
|
49
|
-
|
37
|
+
records = self.update_all!
|
38
|
+
serialized_records = self.bulk_serialize(records)
|
39
|
+
return api_response(serialized_records)
|
50
40
|
end
|
51
41
|
|
52
|
-
# Perform the `update
|
42
|
+
# Perform the `update` call and return the collection of (possibly) updated records.
|
53
43
|
def update_all!
|
54
|
-
|
44
|
+
pk = self.class.get_model.primary_key
|
45
|
+
update_data = if params[:_json].is_a?(Array)
|
46
|
+
self.get_create_params(bulk_mode: :update)[:_json].index_by { |r| r[pk] }
|
47
|
+
else
|
48
|
+
create_params = self.get_create_params
|
49
|
+
{create_params[pk] => create_params}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Perform bulk update in a transaction.
|
53
|
+
return ActiveRecord::Base.transaction do
|
54
|
+
next self.get_recordset.update(update_data.keys, update_data.values)
|
55
|
+
end
|
55
56
|
end
|
56
57
|
end
|
57
58
|
|
58
59
|
# Mixin for destroying records in bulk.
|
59
60
|
module RESTFramework::BulkDestroyModelMixin
|
60
61
|
def destroy_all
|
61
|
-
|
62
|
+
if params[:_json].is_a?(Array)
|
63
|
+
records = self.destroy_all!
|
64
|
+
serialized_records = self.bulk_serialize(records)
|
65
|
+
return api_response(serialized_records)
|
66
|
+
end
|
67
|
+
|
68
|
+
return api_response(
|
69
|
+
{message: "Bulk destroy requires an array of primary keys as input."},
|
70
|
+
status: 400,
|
71
|
+
)
|
62
72
|
end
|
63
73
|
|
64
74
|
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
65
75
|
def destroy_all!
|
66
|
-
|
76
|
+
pk = self.class.get_model.primary_key
|
77
|
+
destroy_data = self.request.request_parameters[:_json]
|
78
|
+
|
79
|
+
# Perform bulk destroy in a transaction.
|
80
|
+
return ActiveRecord::Base.transaction do
|
81
|
+
next self.get_recordset.where(pk => destroy_data).destroy_all
|
82
|
+
end
|
67
83
|
end
|
68
84
|
end
|
69
85
|
|
@@ -79,4 +95,3 @@ module RESTFramework::BulkModelControllerMixin
|
|
79
95
|
RESTFramework::ModelControllerMixin.included(base)
|
80
96
|
end
|
81
97
|
end
|
82
|
-
# :nocov:
|
@@ -66,14 +66,6 @@ module RESTFramework::BaseModelControllerMixin
|
|
66
66
|
|
67
67
|
# Control if filtering is done before find.
|
68
68
|
filter_recordset_before_find: true,
|
69
|
-
|
70
|
-
# Control if bulk operations are done in a transaction and rolled back on error, or if all bulk
|
71
|
-
# operations are attempted and errors simply returned in the response.
|
72
|
-
bulk_transactional: false,
|
73
|
-
|
74
|
-
# Control if bulk operations should be done in "batch" mode, using efficient queries, but also
|
75
|
-
# skipping model validations/callbacks.
|
76
|
-
bulk_batch_mode: false,
|
77
69
|
}
|
78
70
|
|
79
71
|
module ClassMethods
|
@@ -502,15 +494,22 @@ module RESTFramework::BaseModelControllerMixin
|
|
502
494
|
end
|
503
495
|
|
504
496
|
# Use strong parameters to filter the request body using the configured allowed parameters.
|
505
|
-
def get_body_params(data: nil)
|
506
|
-
data ||= request.request_parameters
|
507
|
-
|
508
|
-
|
509
|
-
|
497
|
+
def get_body_params(data: nil, bulk_mode: nil)
|
498
|
+
data ||= self.request.request_parameters
|
499
|
+
pk = self.class.get_model&.primary_key
|
500
|
+
|
501
|
+
# Filter the request body with strong params. If `bulk` is true, then we apply allowed
|
502
|
+
# parameters to the `_json` key of the request body.
|
503
|
+
body_params = if bulk_mode
|
504
|
+
pk = bulk_mode == :update ? [pk] : []
|
505
|
+
ActionController::Parameters.new(data).permit({_json: self.get_allowed_parameters + pk})
|
506
|
+
else
|
507
|
+
ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
|
508
|
+
end
|
510
509
|
|
511
510
|
# Filter primary key if configured.
|
512
|
-
if self.class.filter_pk_from_request_body
|
513
|
-
body_params.delete(
|
511
|
+
if self.class.filter_pk_from_request_body && bulk_mode != :update
|
512
|
+
body_params.delete(pk)
|
514
513
|
end
|
515
514
|
|
516
515
|
# Filter fields in `exclude_body_fields`.
|
@@ -593,6 +592,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
593
592
|
|
594
593
|
recordset = self.get_recordset
|
595
594
|
find_by_key = self.class.get_model.primary_key
|
595
|
+
is_pk = true
|
596
596
|
|
597
597
|
# Find by another column if it's permitted.
|
598
598
|
if find_by_param = self.class.find_by_query_param.presence
|
@@ -600,6 +600,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
600
600
|
find_by_fields = self.get_find_by_fields&.map(&:to_s)
|
601
601
|
|
602
602
|
if !find_by_fields || find_by.in?(find_by_fields)
|
603
|
+
is_pk = false unless find_by_key == find_by
|
603
604
|
find_by_key = find_by
|
604
605
|
end
|
605
606
|
end
|
@@ -611,18 +612,41 @@ module RESTFramework::BaseModelControllerMixin
|
|
611
612
|
end
|
612
613
|
|
613
614
|
# Return the record. Route key is always `:id` by Rails convention.
|
614
|
-
|
615
|
+
if is_pk
|
616
|
+
return @record = recordset.find(request.path_parameters[:id])
|
617
|
+
else
|
618
|
+
return @record = recordset.find_by!(find_by_key => request.path_parameters[:id])
|
619
|
+
end
|
615
620
|
end
|
616
621
|
|
617
|
-
#
|
618
|
-
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
+
# Determine what collection to call `create` on.
|
623
|
+
def get_create_from
|
624
|
+
if self.class.create_from_recordset
|
625
|
+
# Create with any properties inherited from the recordset. We exclude any `select` clauses
|
626
|
+
# in case model callbacks need to call `count` on this collection, which typically raises a
|
627
|
+
# SQL `SyntaxError`.
|
628
|
+
self.get_recordset.except(:select)
|
622
629
|
else
|
623
|
-
|
630
|
+
# Otherwise, perform a "bare" insert_all.
|
631
|
+
self.class.get_model
|
624
632
|
end
|
625
633
|
end
|
634
|
+
|
635
|
+
# Serialize the records, but also include any errors that might exist. This is used for bulk
|
636
|
+
# actions, however we include it here so the helper is available everywhere.
|
637
|
+
def bulk_serialize(records)
|
638
|
+
# This is kinda slow, so perhaps we should eventually integrate `errors` serialization into
|
639
|
+
# the serializer directly. This would fail for active model serializers, but maybe we don't
|
640
|
+
# care?
|
641
|
+
serializer_class = self.get_serializer_class
|
642
|
+
serialized_records = records.map do |record|
|
643
|
+
serializer_class.new(record, controller: self).serialize.merge!(
|
644
|
+
{errors: record.errors.presence}.compact,
|
645
|
+
)
|
646
|
+
end
|
647
|
+
|
648
|
+
return serialized_records
|
649
|
+
end
|
626
650
|
end
|
627
651
|
|
628
652
|
# Mixin for listing records.
|
@@ -668,17 +692,7 @@ module RESTFramework::CreateModelMixin
|
|
668
692
|
|
669
693
|
# Perform the `create!` call and return the created record.
|
670
694
|
def create!
|
671
|
-
|
672
|
-
# Create with any properties inherited from the recordset. We exclude any `select` clauses in
|
673
|
-
# case model callbacks need to call `count` on this collection, which typically raises a SQL
|
674
|
-
# `SyntaxError`.
|
675
|
-
self.get_recordset.except(:select)
|
676
|
-
else
|
677
|
-
# Otherwise, perform a "bare" create.
|
678
|
-
self.class.get_model
|
679
|
-
end
|
680
|
-
|
681
|
-
return create_from.create!(self.get_create_params)
|
695
|
+
return self.get_create_from.create!(self.get_create_params)
|
682
696
|
end
|
683
697
|
end
|
684
698
|
|
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.9.
|
4
|
+
version: 0.9.1
|
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-04-
|
11
|
+
date: 2023-04-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|