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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e2e028b70ec7cad48ee82a18f8e7b68b6c5d2c3d79bf058ab9a9dfc9f1b428b6
4
- data.tar.gz: 70f703c74d2e2d7228f0bf09c8510958a6860bf9a7cc2d7e6f586a8975e0374a
3
+ metadata.gz: '036384f3209aba594f8af47997294c73c4ca95294601d1abf46ee31086610296'
4
+ data.tar.gz: 52424a145b6c959eed3818c23a3e21cd952cb8cb9dc0520506803c08ba5b63b3
5
5
  SHA512:
6
- metadata.gz: 659e40f0cd59dfc249a3799baca4c9a6b0109302904d4874d998c4d656c1ffdd1e0d11f4e463d92cc9006bcb174b7fe72f61bc01e914ed57428616aa56e662bd
7
- data.tar.gz: a172ab0072d67e52280f058ea38694ffd22d543697bcae1a73fc5fc9ec595101d606b4ff681e1f11ba27beca86d3376eda40907b95ae8bf213a6a86c2eb6e3ae
6
+ metadata.gz: 28d9e2ec67ae6ac76f9709a78f7818f96336b87956e447095e48af59386c4cf5375d2fbc9ff243bc5bb915d982a868849a32bda8f3b5406c6fd853e6d2245a58
7
+ data.tar.gz: f8618a040b3ea3cdf44017a2b7dbedd465546f6b073750925635124b97a11e15486883d0902a8259ac8f4d94f4c0a582f6687f1f8148f7332c76342a67c9ff11
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.0
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
- <% @_rrf_form_routes = @route_groups.values[0].select { |r|
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
- <% unless @_rrf_form_routes.empty? %>
162
+ <% if @_rrf_form_routes_raw.present? %>
157
163
  <li class="nav-item">
158
- <a class="nav-link" href="#tab-raw-form" data-bs-toggle="tab" role="tab">
164
+ <a class="nav-link" href="#tabRawForm" data-bs-toggle="tab" role="tab">
159
165
  Raw Form
160
166
  </a>
161
167
  </li>
162
- <% if @is_model_controller %>
163
- <li class="nav-item">
164
- <a class="nav-link" href="#tab-html-form" data-bs-toggle="tab" role="tab">
165
- HTML Form
166
- </a>
167
- </li>
168
- <% end %>
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
- <% unless @_rrf_form_routes.empty? %>
177
- <div class="tab-pane fade" id="tab-raw-form" role="tabpanel">
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
- <% if @is_model_controller %>
181
- <div class="tab-pane fade" id="tab-html-form" role="tabpanel">
182
- <%= render partial: "rest_framework/html_form" %>
183
- </div>
184
- <% end %>
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
- <% @_rrf_form_routes.each do |route| %>
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
- <% @_rrf_form_routes.each do |route| %>
5
+ <% @_rrf_form_routes_raw.each do |route| %>
6
6
  <% path = @route_props[:with_path_args].call(route[:route]) %>
7
- <option value="<%= route[:verb] %>:<%= path %>"><%= route[:verb] %> <%= route[:relative_path] %></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" onchange="rrfCheckRawFilesFormDisplay(this)">
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
- status, payload = self.create_all!
9
- return api_response(payload, status: status)
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` 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`.
23
+ # Perform the `create` call, and return the collection of (possibly) created records.
16
24
  def create_all!
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
25
+ create_data = self.get_create_params(bulk_mode: true)[:_json]
27
26
 
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
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
- raise NotImplementedError, "TODO"
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!` call and return the updated record.
42
+ # Perform the `update` call and return the collection of (possibly) updated records.
53
43
  def update_all!
54
- raise NotImplementedError, "TODO"
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
- raise NotImplementedError, "TODO"
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
- raise NotImplementedError, "TODO"
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
- # Filter the request body with strong params.
509
- body_params = ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
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(self.class.get_model&.primary_key)
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
- return @record = recordset.find_by!(find_by_key => request.path_parameters[:id])
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
- # Create a transaction around the passed block, if configured. This is used primarily for bulk
618
- # actions, but we include it here so it's always available.
619
- def self._rrf_bulk_transaction(&block)
620
- if self.bulk_transactional
621
- ActiveRecord::Base.transaction(&block)
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
- yield
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
- create_from = if self.create_from_recordset && self.get_recordset.respond_to?(:create!)
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.0
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-06 00:00:00.000000000 Z
11
+ date: 2023-04-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails