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 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