rest_framework 0.9.0 → 0.9.1
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/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
|