rest_framework 0.7.4 → 0.7.6
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/rest_framework/_head.html.erb +19 -10
- data/lib/rest_framework/controller_mixins/base.rb +39 -49
- data/lib/rest_framework/controller_mixins/bulk.rb +82 -0
- data/lib/rest_framework/controller_mixins/models.rb +142 -74
- data/lib/rest_framework/controller_mixins.rb +1 -0
- data/lib/rest_framework/filters.rb +66 -13
- data/lib/rest_framework/routers.rb +9 -0
- data/lib/rest_framework/serializers.rb +26 -23
- data/lib/rest_framework/utils.rb +53 -4
- data/lib/rest_framework.rb +19 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: edeb7963dbca0185ff384e628a185c83341e3154d6414c9d8f70a05115abd23a
|
|
4
|
+
data.tar.gz: 8d6bc94fea07e57c20625c8aa192ced88789e078bf3a4b29b71e6c80099f2436
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68ecf8a8a045c3e217c597f86b46573dc80bb931e20be45af01b939079f8cf2dd27d68e8bebd8d458726b04b8b55fb2dd65a60bcf4c847c30feb1257f7cfb9ea
|
|
7
|
+
data.tar.gz: 388478f7c8f2c51a96740f25043aa7d188b5e59731478f4bca2ce6171fadc5ba1e201b12ee051859386ce151c1114b9b6f207dbe840e732dec652d9639323825
|
data/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
0.7.
|
|
1
|
+
0.7.6
|
|
@@ -3,8 +3,20 @@
|
|
|
3
3
|
<%= csrf_meta_tags %>
|
|
4
4
|
<%= csp_meta_tag rescue nil %>
|
|
5
5
|
|
|
6
|
+
<!-- Bootstrap -->
|
|
6
7
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/css/bootstrap.min.css" integrity="sha384-F3w7mX95PdgyTmZZMECAngseQB83DfGTowi0iMjiWaeVhAn4FJkqJByhZMI3AhiU" crossorigin="anonymous">
|
|
7
|
-
<
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script>
|
|
9
|
+
|
|
10
|
+
<!-- Highlight.js -->
|
|
11
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/vs.min.css" integrity="sha512-AVoZ71dJLtHRlsgWwujPT1hk2zxtFWsPlpTPCc/1g0WgpbmlzkqlDFduAvnOV4JJWKUquPc1ZyMc5eq4fRnKOQ==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
|
12
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js" integrity="sha512-bgHRAiTjGrzHzLyKOnpFvaEpGzJet3z4tZnXGjpsCcqOnAH6VGUx9frc5bcIhKTVLEiCO6vEhNAgx5jtLUYrfA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
13
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/json.min.js" integrity="sha512-0xYvyncS9OLE7GOpNBZFnwyh9+bq4HVgk4yVVYI678xRvE22ASicF1v6fZ1UiST+M6pn17MzFZdvVCI3jTHSyw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
14
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/languages/xml.min.js" integrity="sha512-5zBcw+OKRkaNyvUEPlTSfYylVzgpi7KpncY36b0gRudfxIYIH0q0kl2j26uCUB3YBRM6ytQQEZSgRg+ZlBTmdA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
|
15
|
+
|
|
16
|
+
<!-- NeatJSON -->
|
|
17
|
+
<script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
|
|
18
|
+
|
|
19
|
+
<!-- Custom Style -->
|
|
8
20
|
<style>
|
|
9
21
|
/* Adjust headers to always take up their entire row, and tweak the sizing. */
|
|
10
22
|
h1,h2,h3,h4,h5,h6 { display: inline-block; font-weight: normal; margin-bottom: 0; }
|
|
@@ -17,7 +29,7 @@ h6 { font-size: 1rem; }
|
|
|
17
29
|
|
|
18
30
|
/* Make code and code blocks a little nicer looking. */
|
|
19
31
|
code {
|
|
20
|
-
padding:
|
|
32
|
+
padding: .5em !important;
|
|
21
33
|
background-color: #f3f3f3 !important;
|
|
22
34
|
border: 1px solid #aaa;
|
|
23
35
|
border-radius: 3px;
|
|
@@ -56,25 +68,22 @@ code {
|
|
|
56
68
|
}
|
|
57
69
|
</style>
|
|
58
70
|
|
|
59
|
-
|
|
60
|
-
<script src="https://cdn.jsdelivr.net/npm/neatjson@0.10.5/javascript/neatjson.min.js"></script>
|
|
61
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/highlight.min.js" integrity="sha512-TDKKr+IvoqZnPzc3l35hdjpHD0m+b2EC2SrLEgKDRWpxf2rFCxemkgvJ5kfU48ip+Y+m2XVKyOCD85ybtlZDmw==" crossorigin="anonymous"></script>
|
|
62
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/json.min.js" integrity="sha512-FoN8JE+WWCdIGXAIT8KQXwpiavz0Mvjtfk7Rku3MDUNO0BDCiRMXAsSX+e+COFyZTcDb9HDgP+pM2RX12d4j+A==" crossorigin="anonymous"></script>
|
|
63
|
-
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/10.2.0/languages/xml.min.js" integrity="sha512-dICltIgnUP+QSJrnYGCV8943p3qSDgvcg2NU4W8IcOZP4tdrvxlXjbhIznhtVQEcXow0mOjLM0Q6/NvZsmUH4g==" crossorigin="anonymous"></script>
|
|
71
|
+
<!-- Custom JavaScript -->
|
|
64
72
|
<script>
|
|
65
|
-
hljs.initHighlightingOnLoad()
|
|
66
|
-
|
|
67
73
|
// What to do when document loads.
|
|
68
74
|
document.addEventListener("DOMContentLoaded", (event) => {
|
|
69
75
|
// Pretty-print JSON.
|
|
70
76
|
[...document.getElementsByClassName("language-json")].forEach((element, index) => {
|
|
71
|
-
element.innerHTML = neatJSON(JSON.parse(element.
|
|
77
|
+
element.innerHTML = neatJSON(JSON.parse(element.innerText), {
|
|
72
78
|
wrap: 80,
|
|
73
79
|
afterComma: 1,
|
|
74
80
|
afterColon: 1,
|
|
75
81
|
})
|
|
76
82
|
});
|
|
77
83
|
|
|
84
|
+
// Then highlight it.
|
|
85
|
+
hljs.highlightAll();
|
|
86
|
+
|
|
78
87
|
// Insert copy link and callback to copy contents of `<code>` element.
|
|
79
88
|
[...document.getElementsByClassName("rrf-copy")].forEach((element, index) => {
|
|
80
89
|
element.insertAdjacentHTML(
|
|
@@ -12,16 +12,15 @@ module RESTFramework::BaseControllerMixin
|
|
|
12
12
|
:created_at, :created_by, :created_by_id, :updated_at, :updated_by, :updated_by_id
|
|
13
13
|
].freeze,
|
|
14
14
|
accept_generic_params_as_body_params: false,
|
|
15
|
-
show_backtrace: false,
|
|
16
15
|
extra_actions: nil,
|
|
17
16
|
extra_member_actions: nil,
|
|
18
17
|
filter_backends: nil,
|
|
19
18
|
singleton_controller: nil,
|
|
20
19
|
|
|
21
|
-
#
|
|
20
|
+
# Options related to metadata and display.
|
|
22
21
|
title: nil,
|
|
23
22
|
description: nil,
|
|
24
|
-
inflect_acronyms: ["ID", "REST", "API"].freeze,
|
|
23
|
+
inflect_acronyms: ["ID", "IDs", "REST", "API", "APIs"].freeze,
|
|
25
24
|
|
|
26
25
|
# Options related to serialization.
|
|
27
26
|
rescue_unknown_format_with: :json,
|
|
@@ -36,6 +35,10 @@ module RESTFramework::BaseControllerMixin
|
|
|
36
35
|
page_size_query_param: "page_size",
|
|
37
36
|
max_page_size: nil,
|
|
38
37
|
|
|
38
|
+
# Options related to bulk actions and batch processing.
|
|
39
|
+
bulk_guard_query_param: nil,
|
|
40
|
+
enable_batch_processing: nil,
|
|
41
|
+
|
|
39
42
|
# Option to disable serializer adapters by default, mainly introduced because Active Model
|
|
40
43
|
# Serializers will do things like serialize `[]` into `{"":[]}`.
|
|
41
44
|
disable_adapters_by_default: true,
|
|
@@ -74,6 +77,13 @@ module RESTFramework::BaseControllerMixin
|
|
|
74
77
|
end
|
|
75
78
|
end
|
|
76
79
|
|
|
80
|
+
# Add builtin bulk actions.
|
|
81
|
+
RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
|
|
82
|
+
if self.method_defined?(action)
|
|
83
|
+
actions[action] = {path: "", methods: methods, type: :builtin}
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
77
87
|
# Add extra actions.
|
|
78
88
|
if extra_actions = self.try(:extra_actions)
|
|
79
89
|
actions.merge!(RESTFramework::Utils.parse_extra_actions(extra_actions, controller: self))
|
|
@@ -159,11 +169,17 @@ module RESTFramework::BaseControllerMixin
|
|
|
159
169
|
end
|
|
160
170
|
|
|
161
171
|
# Handle some common exceptions.
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
172
|
+
unless RESTFramework.config.disable_rescue_from
|
|
173
|
+
base.rescue_from(
|
|
174
|
+
ActiveRecord::RecordNotFound,
|
|
175
|
+
ActiveRecord::RecordInvalid,
|
|
176
|
+
ActiveRecord::RecordNotSaved,
|
|
177
|
+
ActiveRecord::RecordNotDestroyed,
|
|
178
|
+
ActiveRecord::RecordNotUnique,
|
|
179
|
+
ActiveModel::UnknownAttributeError,
|
|
180
|
+
with: :rrf_error_handler,
|
|
181
|
+
)
|
|
182
|
+
end
|
|
167
183
|
|
|
168
184
|
# Use `TracePoint` hook to automatically call `rrf_finalize`.
|
|
169
185
|
unless RESTFramework.config.disable_auto_finalize
|
|
@@ -211,6 +227,7 @@ module RESTFramework::BaseControllerMixin
|
|
|
211
227
|
|
|
212
228
|
# Filter an arbitrary data set over all configured filter backends.
|
|
213
229
|
def get_filtered_data(data)
|
|
230
|
+
# Apply each filter sequentially.
|
|
214
231
|
self.get_filter_backends.each do |filter_class|
|
|
215
232
|
filter = filter_class.new(controller: self)
|
|
216
233
|
data = filter.get_filtered_data(data)
|
|
@@ -223,53 +240,26 @@ module RESTFramework::BaseControllerMixin
|
|
|
223
240
|
return self.class.get_options_metadata
|
|
224
241
|
end
|
|
225
242
|
|
|
226
|
-
def
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def record_not_found(e)
|
|
236
|
-
return api_response(
|
|
237
|
-
{
|
|
238
|
-
message: "Record not found.",
|
|
239
|
-
}.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
|
|
240
|
-
status: 404,
|
|
241
|
-
)
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
def record_not_saved(e)
|
|
245
|
-
return api_response(
|
|
246
|
-
{
|
|
247
|
-
message: "Record not saved.", errors: e.record&.errors
|
|
248
|
-
}.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
|
|
249
|
-
status: 400,
|
|
250
|
-
)
|
|
251
|
-
end
|
|
252
|
-
|
|
253
|
-
def record_not_destroyed(e)
|
|
254
|
-
return api_response(
|
|
255
|
-
{
|
|
256
|
-
message: "Record not destroyed.", errors: e.record&.errors
|
|
257
|
-
}.merge(self.class.show_backtrace ? {exception: e.full_message} : {}),
|
|
258
|
-
status: 400,
|
|
259
|
-
)
|
|
260
|
-
end
|
|
243
|
+
def rrf_error_handler(e)
|
|
244
|
+
status = case e
|
|
245
|
+
when ActiveRecord::RecordNotFound
|
|
246
|
+
404
|
|
247
|
+
else
|
|
248
|
+
400
|
|
249
|
+
end
|
|
261
250
|
|
|
262
|
-
def unknown_attribute_error(e)
|
|
263
251
|
return api_response(
|
|
264
252
|
{
|
|
265
|
-
message: e.message
|
|
266
|
-
|
|
267
|
-
|
|
253
|
+
message: e.message,
|
|
254
|
+
errors: e.try(:record).try(:errors),
|
|
255
|
+
exception: RESTFramework.config.show_backtrace ? e.full_message : nil,
|
|
256
|
+
}.compact,
|
|
257
|
+
status: status,
|
|
268
258
|
)
|
|
269
259
|
end
|
|
270
260
|
|
|
271
|
-
#
|
|
272
|
-
#
|
|
261
|
+
# Render a browsable API for `html` format, along with basic `json`/`xml` formats, and with
|
|
262
|
+
# support or passing custom `kwargs` to the underlying `render` calls.
|
|
273
263
|
def api_response(payload, html_kwargs: nil, **kwargs)
|
|
274
264
|
html_kwargs ||= {}
|
|
275
265
|
json_kwargs = kwargs.delete(:json_kwargs) || {}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
require_relative "models"
|
|
2
|
+
|
|
3
|
+
# Mixin for creating records in bulk. This is unique compared to update/destroy because we overload
|
|
4
|
+
# the existing `create` action to support bulk creation.
|
|
5
|
+
# :nocov:
|
|
6
|
+
module RESTFramework::BulkCreateModelMixin
|
|
7
|
+
def create
|
|
8
|
+
status, payload = self.create_all!
|
|
9
|
+
return api_response(payload, status: status)
|
|
10
|
+
end
|
|
11
|
+
|
|
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`.
|
|
16
|
+
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
|
|
27
|
+
|
|
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
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Mixin for updating records in bulk.
|
|
47
|
+
module RESTFramework::BulkUpdateModelMixin
|
|
48
|
+
def update_all
|
|
49
|
+
raise NotImplementedError, "TODO"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Perform the `update!` call and return the updated record.
|
|
53
|
+
def update_all!
|
|
54
|
+
raise NotImplementedError, "TODO"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Mixin for destroying records in bulk.
|
|
59
|
+
module RESTFramework::BulkDestroyModelMixin
|
|
60
|
+
def destroy_all
|
|
61
|
+
raise NotImplementedError, "TODO"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
|
65
|
+
def destroy_all!
|
|
66
|
+
raise NotImplementedError, "TODO"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Mixin that includes all the CRUD bulk mixins.
|
|
71
|
+
module RESTFramework::BulkModelControllerMixin
|
|
72
|
+
include RESTFramework::ModelControllerMixin
|
|
73
|
+
|
|
74
|
+
include RESTFramework::BulkCreateModelMixin
|
|
75
|
+
include RESTFramework::BulkUpdateModelMixin
|
|
76
|
+
include RESTFramework::BulkDestroyModelMixin
|
|
77
|
+
|
|
78
|
+
def self.included(base)
|
|
79
|
+
RESTFramework::ModelControllerMixin.included(base)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
# :nocov:
|
|
@@ -12,6 +12,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
12
12
|
|
|
13
13
|
# Attributes for configuring record fields.
|
|
14
14
|
fields: nil,
|
|
15
|
+
field_config: nil,
|
|
15
16
|
action_fields: nil,
|
|
16
17
|
|
|
17
18
|
# Attributes for finding records.
|
|
@@ -38,9 +39,22 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
38
39
|
search_query_param: "search",
|
|
39
40
|
search_ilike: false,
|
|
40
41
|
|
|
41
|
-
#
|
|
42
|
-
create_from_recordset: true,
|
|
43
|
-
|
|
42
|
+
# Option for `recordset.create` vs `Model.create` behavior.
|
|
43
|
+
create_from_recordset: true,
|
|
44
|
+
|
|
45
|
+
# Control if filtering is done before find.
|
|
46
|
+
filter_recordset_before_find: true,
|
|
47
|
+
|
|
48
|
+
# Option to exclude associations from default fields.
|
|
49
|
+
exclude_associations: false,
|
|
50
|
+
|
|
51
|
+
# Control if bulk operations are done in a transaction and rolled back on error, or if all bulk
|
|
52
|
+
# operations are attempted and errors simply returned in the response.
|
|
53
|
+
bulk_transactional: false,
|
|
54
|
+
|
|
55
|
+
# Control if bulk operations should be done in "batch" mode, using efficient queries, but also
|
|
56
|
+
# skipping model validations/callbacks.
|
|
57
|
+
bulk_batch_mode: false,
|
|
44
58
|
}
|
|
45
59
|
|
|
46
60
|
module ClassMethods
|
|
@@ -77,14 +91,54 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
77
91
|
return self.get_model.human_attribute_name(s, default: super)
|
|
78
92
|
end
|
|
79
93
|
|
|
94
|
+
# Get fields without any action context. Always fallback to columns at the class level.
|
|
95
|
+
def get_fields
|
|
96
|
+
if self.fields.is_a?(Hash)
|
|
97
|
+
return RESTFramework::Utils.parse_fields_hash(
|
|
98
|
+
self.fields, self.get_model, exclude_associations: self.exclude_associations
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
return self.fields || (
|
|
103
|
+
self.get_model ? RESTFramework::Utils.fields_for(
|
|
104
|
+
self.get_model, exclude_associations: self.exclude_associations
|
|
105
|
+
) : []
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Get a field's config, including defaults.
|
|
110
|
+
def get_field_config(f)
|
|
111
|
+
config = self.field_config&.dig(f.to_sym) || {}
|
|
112
|
+
|
|
113
|
+
# Default sub-fields if field is an association.
|
|
114
|
+
if ref = self.get_model.reflections[f]
|
|
115
|
+
model = ref.klass
|
|
116
|
+
columns = model.columns_hash
|
|
117
|
+
config[:sub_fields] ||= RESTFramework::Utils.sub_fields_for(ref)
|
|
118
|
+
|
|
119
|
+
# Serialize very basic metadata about sub-fields.
|
|
120
|
+
config[:sub_fields_metadata] = config[:sub_fields].map { |sf|
|
|
121
|
+
v = {}
|
|
122
|
+
|
|
123
|
+
if columns[sf]
|
|
124
|
+
v[:kind] = "column"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
next [sf, v]
|
|
128
|
+
}.to_h.compact.presence
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
return config.compact
|
|
132
|
+
end
|
|
133
|
+
|
|
80
134
|
# Get metadata about the resource's fields.
|
|
81
|
-
def get_fields_metadata
|
|
135
|
+
def get_fields_metadata
|
|
82
136
|
# Get metadata sources.
|
|
83
137
|
model = self.get_model
|
|
84
|
-
fields
|
|
85
|
-
fields = fields.map(&:to_s)
|
|
138
|
+
fields = self.get_fields.map(&:to_s)
|
|
86
139
|
columns = model.columns_hash
|
|
87
140
|
column_defaults = model.column_defaults
|
|
141
|
+
reflections = model.reflections
|
|
88
142
|
attributes = model._default_attributes
|
|
89
143
|
|
|
90
144
|
return fields.map { |f|
|
|
@@ -105,9 +159,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
105
159
|
|
|
106
160
|
# Determine `type`, `required`, `label`, and `kind` based on schema.
|
|
107
161
|
if column = columns[f]
|
|
162
|
+
metadata[:kind] = "column"
|
|
108
163
|
metadata[:type] = column.type
|
|
109
164
|
metadata[:required] = true unless column.null
|
|
110
|
-
metadata[:kind] = "column"
|
|
111
165
|
end
|
|
112
166
|
|
|
113
167
|
# Determine `default` based on schema; we use `column_defaults` rather than `columns_hash`
|
|
@@ -117,25 +171,46 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
117
171
|
metadata[:default] = column_default
|
|
118
172
|
end
|
|
119
173
|
|
|
120
|
-
#
|
|
174
|
+
# Extract details from the model's attributes hash.
|
|
121
175
|
if attributes.key?(f) && attribute = attributes[f]
|
|
122
176
|
unless metadata.key?(:default)
|
|
123
177
|
default = attribute.value_before_type_cast
|
|
124
178
|
metadata[:default] = default unless default.nil?
|
|
125
179
|
end
|
|
180
|
+
metadata[:kind] ||= "attribute"
|
|
181
|
+
|
|
182
|
+
# Get any type information from the attribute.
|
|
183
|
+
if type = attribute.type
|
|
184
|
+
metadata[:type] ||= type.type
|
|
126
185
|
|
|
127
|
-
|
|
128
|
-
|
|
186
|
+
# Get enum variants.
|
|
187
|
+
if type.is_a?(ActiveRecord::Enum::EnumType)
|
|
188
|
+
metadata[:enum_variants] = type.send(:mapping)
|
|
189
|
+
end
|
|
129
190
|
end
|
|
130
191
|
end
|
|
131
192
|
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
193
|
+
# Get association metadata.
|
|
194
|
+
if ref = reflections[f]
|
|
195
|
+
metadata[:kind] = "association"
|
|
196
|
+
begin
|
|
197
|
+
pk = ref.active_record_primary_key
|
|
198
|
+
rescue ActiveRecord::UnknownPrimaryKey
|
|
138
199
|
end
|
|
200
|
+
metadata[:association] = {
|
|
201
|
+
macro: ref.macro,
|
|
202
|
+
class_name: ref.class_name,
|
|
203
|
+
foreign_key: ref.foreign_key,
|
|
204
|
+
primary_key: pk,
|
|
205
|
+
polymorphic: ref.polymorphic?,
|
|
206
|
+
table_name: ref.table_name,
|
|
207
|
+
options: ref.options.presence,
|
|
208
|
+
}.compact
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Determine if this is just a method.
|
|
212
|
+
if model.method_defined?(f)
|
|
213
|
+
metadata[:kind] ||= "method"
|
|
139
214
|
end
|
|
140
215
|
|
|
141
216
|
# Collect validator options into a hash on their type, while also updating `required` based
|
|
@@ -156,32 +231,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
156
231
|
metadata[:validators][kind] << options
|
|
157
232
|
end
|
|
158
233
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
end
|
|
234
|
+
# Serialize any field config.
|
|
235
|
+
metadata[:config] = self.get_field_config(f).presence
|
|
162
236
|
|
|
163
|
-
|
|
164
|
-
def get_associations_metadata
|
|
165
|
-
return self.get_model.reflections.map { |k, v|
|
|
166
|
-
next [k, {
|
|
167
|
-
macro: v.macro,
|
|
168
|
-
label: self.get_label(k),
|
|
169
|
-
class_name: v.class_name,
|
|
170
|
-
foreign_key: v.foreign_key,
|
|
171
|
-
primary_key: v.active_record_primary_key,
|
|
172
|
-
polymorphic: v.polymorphic?,
|
|
173
|
-
table_name: v.table_name,
|
|
174
|
-
options: v.options,
|
|
175
|
-
}.compact]
|
|
237
|
+
next [f, metadata.compact]
|
|
176
238
|
}.to_h
|
|
177
239
|
end
|
|
178
240
|
|
|
179
241
|
# Get a hash of metadata to be rendered in the `OPTIONS` response. Cache the result.
|
|
180
|
-
def get_options_metadata
|
|
242
|
+
def get_options_metadata
|
|
181
243
|
return super().merge(
|
|
182
244
|
{
|
|
183
|
-
fields: self.get_fields_metadata
|
|
184
|
-
associations: self.get_associations_metadata,
|
|
245
|
+
fields: self.get_fields_metadata,
|
|
185
246
|
},
|
|
186
247
|
)
|
|
187
248
|
end
|
|
@@ -238,9 +299,10 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
238
299
|
end
|
|
239
300
|
|
|
240
301
|
def self.included(base)
|
|
302
|
+
RESTFramework::BaseControllerMixin.included(base)
|
|
303
|
+
|
|
241
304
|
return unless base.is_a?(Class)
|
|
242
305
|
|
|
243
|
-
RESTFramework::BaseControllerMixin.included(base)
|
|
244
306
|
base.extend(ClassMethods)
|
|
245
307
|
|
|
246
308
|
# Add class attributes (with defaults) unless they already exist.
|
|
@@ -265,27 +327,23 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
265
327
|
return (action_config[action] if action) || self.class.send(generic_config_key)
|
|
266
328
|
end
|
|
267
329
|
|
|
268
|
-
# Get fields without any action context. Always fallback to columns at the class level.
|
|
269
|
-
def self.get_fields
|
|
270
|
-
if self.fields.is_a?(Hash)
|
|
271
|
-
return RESTFramework::Utils.parse_fields_hash(self.fields, self.get_model)
|
|
272
|
-
end
|
|
273
|
-
|
|
274
|
-
return self.fields || self.get_model&.column_names || []
|
|
275
|
-
end
|
|
276
|
-
|
|
277
330
|
# Get a list of fields for the current action. Returning `nil` indicates that anything should be
|
|
278
331
|
# accepted unless `fallback` is true, in which case we should fallback to this controller's model
|
|
279
332
|
# columns, or en empty array.
|
|
280
333
|
def get_fields(fallback: false)
|
|
281
334
|
fields = _get_specific_action_config(:action_fields, :fields)
|
|
282
335
|
|
|
283
|
-
# If fields is a hash, then parse
|
|
336
|
+
# If fields is a hash, then parse it.
|
|
284
337
|
if fields.is_a?(Hash)
|
|
285
|
-
return RESTFramework::Utils.parse_fields_hash(
|
|
338
|
+
return RESTFramework::Utils.parse_fields_hash(
|
|
339
|
+
fields, self.class.get_model, exclude_associations: self.class.exclude_associations
|
|
340
|
+
)
|
|
286
341
|
elsif !fields && fallback
|
|
287
342
|
# Otherwise, if fields is nil and fallback is true, then fallback to columns.
|
|
288
|
-
|
|
343
|
+
model = self.class.get_model
|
|
344
|
+
return model ? RESTFramework::Utils.fields_for(
|
|
345
|
+
model, exclude_associations: self.class.exclude_associations
|
|
346
|
+
) : []
|
|
289
347
|
end
|
|
290
348
|
|
|
291
349
|
return fields
|
|
@@ -293,7 +351,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
293
351
|
|
|
294
352
|
# Pass fields to get dynamic metadata based on which fields are available.
|
|
295
353
|
def get_options_metadata
|
|
296
|
-
return self.class.get_options_metadata
|
|
354
|
+
return self.class.get_options_metadata
|
|
297
355
|
end
|
|
298
356
|
|
|
299
357
|
# Get a list of find_by fields for the current action. Do not fallback to columns in case the user
|
|
@@ -311,12 +369,12 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
311
369
|
) || self.get_fields
|
|
312
370
|
end
|
|
313
371
|
|
|
314
|
-
#
|
|
372
|
+
# Get the configured serializer class, or `NativeSerializer` as a default.
|
|
315
373
|
def get_serializer_class
|
|
316
374
|
return super || RESTFramework::NativeSerializer
|
|
317
375
|
end
|
|
318
376
|
|
|
319
|
-
#
|
|
377
|
+
# Get filtering backends, defaulting to using `ModelFilter` and `ModelOrderingFilter`.
|
|
320
378
|
def get_filter_backends
|
|
321
379
|
return self.class.filter_backends || [
|
|
322
380
|
RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
|
|
@@ -324,14 +382,16 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
324
382
|
end
|
|
325
383
|
|
|
326
384
|
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
|
327
|
-
def get_body_params
|
|
385
|
+
def get_body_params(data: nil)
|
|
386
|
+
data ||= request.request_parameters
|
|
387
|
+
|
|
328
388
|
# Filter the request body and map to strings. Return all params if we cannot resolve a list of
|
|
329
389
|
# allowed parameters or fields.
|
|
330
390
|
allowed_params = self.get_allowed_parameters&.map(&:to_s)
|
|
331
391
|
body_params = if allowed_params
|
|
332
|
-
|
|
392
|
+
data.select { |p| allowed_params.include?(p) }
|
|
333
393
|
else
|
|
334
|
-
|
|
394
|
+
data
|
|
335
395
|
end
|
|
336
396
|
|
|
337
397
|
# Add query params in place of missing body params, if configured.
|
|
@@ -370,7 +430,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
370
430
|
return @recordset = nil
|
|
371
431
|
end
|
|
372
432
|
|
|
373
|
-
#
|
|
433
|
+
# Get the records this controller has access to *after* any filtering is applied.
|
|
374
434
|
def get_records
|
|
375
435
|
return @records if instance_variable_defined?(:@records)
|
|
376
436
|
|
|
@@ -403,7 +463,17 @@ module RESTFramework::BaseModelControllerMixin
|
|
|
403
463
|
end
|
|
404
464
|
|
|
405
465
|
# Return the record. Route key is always `:id` by Rails convention.
|
|
406
|
-
return @record = recordset.find_by!(find_by_key =>
|
|
466
|
+
return @record = recordset.find_by!(find_by_key => request.path_parameters[:id])
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Create a transaction around the passed block, if configured. This is used primarily for bulk
|
|
470
|
+
# actions, but we include it here so it's always available.
|
|
471
|
+
def self._rrf_bulk_transaction(&block)
|
|
472
|
+
if self.bulk_transactional
|
|
473
|
+
ActiveRecord::Base.transaction(&block)
|
|
474
|
+
else
|
|
475
|
+
yield
|
|
476
|
+
end
|
|
407
477
|
end
|
|
408
478
|
end
|
|
409
479
|
|
|
@@ -413,7 +483,7 @@ module RESTFramework::ListModelMixin
|
|
|
413
483
|
return api_response(self.get_index_records)
|
|
414
484
|
end
|
|
415
485
|
|
|
416
|
-
#
|
|
486
|
+
# Get records with both filtering and pagination applied.
|
|
417
487
|
def get_index_records
|
|
418
488
|
records = self.get_records
|
|
419
489
|
|
|
@@ -448,17 +518,19 @@ module RESTFramework::CreateModelMixin
|
|
|
448
518
|
return api_response(self.create!, status: :created)
|
|
449
519
|
end
|
|
450
520
|
|
|
451
|
-
#
|
|
521
|
+
# Perform the `create!` call and return the created record.
|
|
452
522
|
def create!
|
|
453
|
-
if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
|
|
523
|
+
create_from = if self.get_recordset.respond_to?(:create!) && self.create_from_recordset
|
|
454
524
|
# Create with any properties inherited from the recordset. We exclude any `select` clauses in
|
|
455
525
|
# case model callbacks need to call `count` on this collection, which typically raises a SQL
|
|
456
526
|
# `SyntaxError`.
|
|
457
|
-
|
|
527
|
+
self.get_recordset.except(:select)
|
|
458
528
|
else
|
|
459
529
|
# Otherwise, perform a "bare" create.
|
|
460
|
-
|
|
530
|
+
self.class.get_model
|
|
461
531
|
end
|
|
532
|
+
|
|
533
|
+
return create_from.create!(self.get_create_params)
|
|
462
534
|
end
|
|
463
535
|
end
|
|
464
536
|
|
|
@@ -468,7 +540,7 @@ module RESTFramework::UpdateModelMixin
|
|
|
468
540
|
return api_response(self.update!)
|
|
469
541
|
end
|
|
470
542
|
|
|
471
|
-
#
|
|
543
|
+
# Perform the `update!` call and return the updated record.
|
|
472
544
|
def update!
|
|
473
545
|
record = self.get_record
|
|
474
546
|
record.update!(self.get_update_params)
|
|
@@ -483,7 +555,7 @@ module RESTFramework::DestroyModelMixin
|
|
|
483
555
|
return api_response("")
|
|
484
556
|
end
|
|
485
557
|
|
|
486
|
-
#
|
|
558
|
+
# Perform the `destroy!` call and return the destroyed (and frozen) record.
|
|
487
559
|
def destroy!
|
|
488
560
|
return self.get_record.destroy!
|
|
489
561
|
end
|
|
@@ -493,29 +565,25 @@ end
|
|
|
493
565
|
module RESTFramework::ReadOnlyModelControllerMixin
|
|
494
566
|
include RESTFramework::BaseModelControllerMixin
|
|
495
567
|
|
|
496
|
-
|
|
497
|
-
|
|
568
|
+
include RESTFramework::ListModelMixin
|
|
569
|
+
include RESTFramework::ShowModelMixin
|
|
498
570
|
|
|
571
|
+
def self.included(base)
|
|
499
572
|
RESTFramework::BaseModelControllerMixin.included(base)
|
|
500
573
|
end
|
|
501
|
-
|
|
502
|
-
include RESTFramework::ListModelMixin
|
|
503
|
-
include RESTFramework::ShowModelMixin
|
|
504
574
|
end
|
|
505
575
|
|
|
506
576
|
# Mixin that includes all the CRUD mixins.
|
|
507
577
|
module RESTFramework::ModelControllerMixin
|
|
508
578
|
include RESTFramework::BaseModelControllerMixin
|
|
509
579
|
|
|
510
|
-
def self.included(base)
|
|
511
|
-
return unless base.is_a?(Class)
|
|
512
|
-
|
|
513
|
-
RESTFramework::BaseModelControllerMixin.included(base)
|
|
514
|
-
end
|
|
515
|
-
|
|
516
580
|
include RESTFramework::ListModelMixin
|
|
517
581
|
include RESTFramework::ShowModelMixin
|
|
518
582
|
include RESTFramework::CreateModelMixin
|
|
519
583
|
include RESTFramework::UpdateModelMixin
|
|
520
584
|
include RESTFramework::DestroyModelMixin
|
|
585
|
+
|
|
586
|
+
def self.included(base)
|
|
587
|
+
RESTFramework::BaseModelControllerMixin.included(base)
|
|
588
|
+
end
|
|
521
589
|
end
|
|
@@ -14,23 +14,59 @@ class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
|
|
14
14
|
# Get a list of filterset fields for the current action. Fallback to columns because we don't want
|
|
15
15
|
# to try filtering by any query parameter because that could clash with other query parameters.
|
|
16
16
|
def _get_fields
|
|
17
|
-
return @
|
|
17
|
+
return @_get_fields ||= (
|
|
18
|
+
@controller.class.filterset_fields || @controller.get_fields(fallback: true)
|
|
19
|
+
).map(&:to_s)
|
|
18
20
|
end
|
|
19
21
|
|
|
20
22
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
|
21
23
|
def _get_filter_params
|
|
22
24
|
# Map filterset fields to strings because query parameter keys are strings.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
fields = self._get_fields
|
|
26
|
+
@associations = []
|
|
27
|
+
|
|
28
|
+
return @controller.request.query_parameters.select { |p, _|
|
|
29
|
+
# Remove any trailing `__in` from the field name.
|
|
30
|
+
field = p.chomp("__in")
|
|
31
|
+
|
|
32
|
+
# Remove any associations whose sub-fields are not filterable. Also populate `@associations`
|
|
33
|
+
# so the caller can include them.
|
|
34
|
+
if match = /(.*)\.(.*)/.match(field)
|
|
35
|
+
field, sub_field = match[1..2]
|
|
36
|
+
next false unless field.in?(fields)
|
|
37
|
+
|
|
38
|
+
sub_fields = @controller.class.get_field_config(field)[:sub_fields]
|
|
39
|
+
if sub_field.in?(sub_fields)
|
|
40
|
+
@associations << field.to_sym
|
|
41
|
+
next true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
next false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
next field.in?(fields)
|
|
48
|
+
}.map { |p, v|
|
|
49
|
+
# Convert fields ending in `__in` to array values.
|
|
50
|
+
if p.end_with?("__in")
|
|
51
|
+
p = p.chomp("__in")
|
|
52
|
+
v = v.split(",")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Convert "nil" and "null" to nil.
|
|
56
|
+
if v == "nil" || v == "null"
|
|
57
|
+
v = nil
|
|
58
|
+
end
|
|
26
59
|
|
|
27
|
-
|
|
60
|
+
[p, v]
|
|
61
|
+
}.to_h.symbolize_keys
|
|
28
62
|
end
|
|
29
63
|
|
|
30
64
|
# Filter data according to the request query parameters.
|
|
31
65
|
def get_filtered_data(data)
|
|
32
|
-
filter_params = self._get_filter_params.
|
|
33
|
-
|
|
66
|
+
if filter_params = self._get_filter_params.presence
|
|
67
|
+
# Include any associations.
|
|
68
|
+
data = data.includes(*@associations) unless @associations.empty?
|
|
69
|
+
|
|
34
70
|
return data.where(**filter_params)
|
|
35
71
|
end
|
|
36
72
|
|
|
@@ -43,18 +79,22 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
|
43
79
|
# Get a list of ordering fields for the current action. Do not fallback to columns in case the
|
|
44
80
|
# user wants to order by a virtual column.
|
|
45
81
|
def _get_fields
|
|
46
|
-
return @
|
|
82
|
+
return @_get_fields ||= (
|
|
83
|
+
@controller.class.ordering_fields || @controller.get_fields
|
|
84
|
+
)&.map(&:to_s)
|
|
47
85
|
end
|
|
48
86
|
|
|
49
87
|
# Convert ordering string to an ordering configuration.
|
|
50
88
|
def _get_ordering
|
|
51
89
|
return nil if @controller.class.ordering_query_param.blank?
|
|
52
90
|
|
|
91
|
+
@associations = []
|
|
92
|
+
|
|
53
93
|
# Ensure ordering_fields are strings since the split param will be strings.
|
|
54
|
-
fields = self._get_fields
|
|
94
|
+
fields = self._get_fields
|
|
55
95
|
order_string = @controller.params[@controller.class.ordering_query_param]
|
|
56
96
|
|
|
57
|
-
if order_string.present?
|
|
97
|
+
if order_string.present?
|
|
58
98
|
ordering = {}.with_indifferent_access
|
|
59
99
|
order_string.split(",").each do |field|
|
|
60
100
|
if field[0] == "-"
|
|
@@ -64,9 +104,19 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
|
64
104
|
column = field
|
|
65
105
|
direction = :asc
|
|
66
106
|
end
|
|
67
|
-
|
|
68
|
-
|
|
107
|
+
next unless !fields || column.in?(fields)
|
|
108
|
+
|
|
109
|
+
# Populate any `@associations` so the caller can include them.
|
|
110
|
+
if match = /(.*)\.(.*)/.match(column)
|
|
111
|
+
association, sub_field = match[1..2]
|
|
112
|
+
@associations << association.to_sym
|
|
113
|
+
|
|
114
|
+
# Also, due to Rails weirdness, we need to convert the association name to the table name.
|
|
115
|
+
table_name = @controller.class.get_model.reflections[association].table_name
|
|
116
|
+
column = "#{table_name}.#{sub_field}"
|
|
69
117
|
end
|
|
118
|
+
|
|
119
|
+
ordering[column] = direction
|
|
70
120
|
end
|
|
71
121
|
return ordering
|
|
72
122
|
end
|
|
@@ -80,7 +130,10 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
|
80
130
|
reorder = !@controller.class.ordering_no_reorder
|
|
81
131
|
|
|
82
132
|
if ordering && !ordering.empty?
|
|
83
|
-
|
|
133
|
+
# Include any associations.
|
|
134
|
+
data = data.includes(*@associations) unless @associations.empty?
|
|
135
|
+
|
|
136
|
+
return data.send(reorder ? :reorder : :order, ordering)
|
|
84
137
|
end
|
|
85
138
|
|
|
86
139
|
return data
|
|
@@ -102,6 +102,15 @@ module ActionDispatch::Routing
|
|
|
102
102
|
public_send(m, "", action: action) if self.respond_to?(m)
|
|
103
103
|
end
|
|
104
104
|
end
|
|
105
|
+
|
|
106
|
+
# Route bulk actions, if configured.
|
|
107
|
+
RESTFramework::RRF_BUILTIN_BULK_ACTIONS.each do |action, methods|
|
|
108
|
+
next unless controller_class.method_defined?(action)
|
|
109
|
+
|
|
110
|
+
[methods].flatten.each do |m|
|
|
111
|
+
public_send(m, "", action: action) if self.respond_to?(m)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
105
114
|
end
|
|
106
115
|
|
|
107
116
|
if unscoped
|
|
@@ -88,7 +88,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
88
88
|
return self.config || self.singular_config || self.plural_config
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
-
#
|
|
91
|
+
# Get a native serializer configuration from the controller.
|
|
92
92
|
def get_controller_native_serializer_config
|
|
93
93
|
return nil unless @controller
|
|
94
94
|
|
|
@@ -101,9 +101,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
101
101
|
return controller_serializer || @controller.class.try(:native_serializer_config)
|
|
102
102
|
end
|
|
103
103
|
|
|
104
|
-
#
|
|
105
|
-
#
|
|
106
|
-
# behavior:
|
|
104
|
+
# Filter a single subconfig for specific keys. By default, keys from `fields` are removed from the
|
|
105
|
+
# provided `subcfg`. There are two (mutually exclusive) options to adjust the behavior:
|
|
107
106
|
#
|
|
108
107
|
# `add`: Add any `fields` to the `subcfg` which aren't already in the `subcfg`.
|
|
109
108
|
# `only`: Remove any values found in the `subcfg` not in `fields`.
|
|
@@ -149,16 +148,14 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
149
148
|
return subcfg
|
|
150
149
|
end
|
|
151
150
|
|
|
152
|
-
#
|
|
153
|
-
def
|
|
151
|
+
# Filter out configuration properties based on the :except/:only query parameters.
|
|
152
|
+
def filter_from_request(cfg)
|
|
154
153
|
return cfg unless @controller
|
|
155
154
|
|
|
156
155
|
except_param = @controller.class.try(:native_serializer_except_query_param)
|
|
157
156
|
only_param = @controller.class.try(:native_serializer_only_query_param)
|
|
158
157
|
if except_param && except = @controller.request.query_parameters[except_param].presence
|
|
159
|
-
except = except.split(",").map(&:strip).map(&:to_sym)
|
|
160
|
-
|
|
161
|
-
unless except.empty?
|
|
158
|
+
if except = except.split(",").map(&:strip).map(&:to_sym).presence
|
|
162
159
|
# Filter `only`, `except` (additive), `include`, `methods`, and `serializer_methods`.
|
|
163
160
|
if cfg[:only]
|
|
164
161
|
cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: except)
|
|
@@ -167,6 +164,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
167
164
|
else
|
|
168
165
|
cfg[:except] = except
|
|
169
166
|
end
|
|
167
|
+
|
|
170
168
|
cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: except)
|
|
171
169
|
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: except)
|
|
172
170
|
cfg[:serializer_methods] = self.class.filter_subcfg(
|
|
@@ -174,20 +172,15 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
174
172
|
)
|
|
175
173
|
end
|
|
176
174
|
elsif only_param && only = @controller.request.query_parameters[only_param].presence
|
|
177
|
-
only = only.split(",").map(&:strip).map(&:to_sym)
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
# Filter `only`, `except` (additive), `include`, and `methods`.
|
|
175
|
+
if only = only.split(",").map(&:strip).map(&:to_sym).presence
|
|
176
|
+
# Filter `only`, `include`, and `methods`. Adding anything to `except` is not needed,
|
|
177
|
+
# because any configuration there takes precedence over `only`.
|
|
181
178
|
if cfg[:only]
|
|
182
179
|
cfg[:only] = self.class.filter_subcfg(cfg[:only], fields: only, only: true)
|
|
183
|
-
elsif cfg[:except]
|
|
184
|
-
# For the `except` part of the serializer, we need to append any columns not in `only`.
|
|
185
|
-
model = @controller.class.get_model
|
|
186
|
-
except_cols = model&.column_names&.map(&:to_sym)&.reject { |c| c.in?(only) }
|
|
187
|
-
cfg[:except] = self.class.filter_subcfg(cfg[:except], fields: except_cols, add: true)
|
|
188
180
|
else
|
|
189
181
|
cfg[:only] = only
|
|
190
182
|
end
|
|
183
|
+
|
|
191
184
|
cfg[:include] = self.class.filter_subcfg(cfg[:include], fields: only, only: true)
|
|
192
185
|
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], fields: only, only: true)
|
|
193
186
|
cfg[:serializer_methods] = self.class.filter_subcfg(
|
|
@@ -213,18 +206,28 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
213
206
|
end
|
|
214
207
|
|
|
215
208
|
# If the config wasn't determined, build a serializer config from controller fields.
|
|
216
|
-
if fields = @controller&.get_fields
|
|
209
|
+
if fields = @controller&.get_fields(fallback: true)
|
|
217
210
|
fields = fields.deep_dup
|
|
218
211
|
|
|
219
212
|
columns = []
|
|
220
|
-
includes =
|
|
213
|
+
includes = {}
|
|
221
214
|
methods = []
|
|
222
215
|
if @model
|
|
223
216
|
fields.each do |f|
|
|
224
217
|
if f.in?(@model.column_names)
|
|
225
218
|
columns << f
|
|
226
219
|
elsif @model.reflections.key?(f)
|
|
227
|
-
|
|
220
|
+
sub_columns = []
|
|
221
|
+
sub_methods = []
|
|
222
|
+
@controller.class.get_field_config(f)[:sub_fields].each do |sf|
|
|
223
|
+
sub_model = @model.reflections[f].klass
|
|
224
|
+
if sf.in?(sub_model.column_names)
|
|
225
|
+
sub_columns << sf
|
|
226
|
+
elsif sub_model.method_defined?(sf)
|
|
227
|
+
sub_methods << sf
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
includes[f] = {only: sub_columns, methods: sub_methods}
|
|
228
231
|
elsif @model.method_defined?(f)
|
|
229
232
|
methods << f
|
|
230
233
|
end
|
|
@@ -242,10 +245,10 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
|
242
245
|
|
|
243
246
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
|
244
247
|
def get_serializer_config
|
|
245
|
-
return
|
|
248
|
+
return filter_from_request(self._get_raw_serializer_config)
|
|
246
249
|
end
|
|
247
250
|
|
|
248
|
-
#
|
|
251
|
+
# Serialize a single record and merge results of `serializer_methods`.
|
|
249
252
|
def _serialize(record, config, serializer_methods)
|
|
250
253
|
# Ensure serializer_methods is either falsy, or an array.
|
|
251
254
|
if serializer_methods && !serializer_methods.respond_to?(:to_ary)
|
data/lib/rest_framework/utils.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module RESTFramework::Utils
|
|
2
2
|
HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE OPTIONS HEAD)
|
|
3
|
+
LABEL_FIELDS = %w(name label login title email username)
|
|
3
4
|
|
|
4
5
|
# Convert `extra_actions` hash to a consistent format: `{path:, methods:, kwargs:}`, and
|
|
5
6
|
# additional metadata fields.
|
|
@@ -139,15 +140,63 @@ module RESTFramework::Utils
|
|
|
139
140
|
end
|
|
140
141
|
|
|
141
142
|
# Parse fields hashes.
|
|
142
|
-
def self.parse_fields_hash(fields_hash, model)
|
|
143
|
-
parsed_fields = fields_hash[:only] ||
|
|
144
|
-
|
|
143
|
+
def self.parse_fields_hash(fields_hash, model, exclude_associations: nil)
|
|
144
|
+
parsed_fields = fields_hash[:only] || (
|
|
145
|
+
model ? self.fields_for(model, exclude_associations: exclude_associations) : []
|
|
146
|
+
)
|
|
147
|
+
parsed_fields += fields_hash[:include] if fields_hash[:include]
|
|
148
|
+
parsed_fields -= fields_hash[:exclude] if fields_hash[:exclude]
|
|
145
149
|
|
|
146
150
|
# Warn for any unknown keys.
|
|
147
|
-
(fields_hash.keys - [:only, :
|
|
151
|
+
(fields_hash.keys - [:only, :include, :exclude]).each do |k|
|
|
148
152
|
Rails.logger.warn("RRF: Unknown key in fields hash: #{k}")
|
|
149
153
|
end
|
|
150
154
|
|
|
151
155
|
return parsed_fields
|
|
152
156
|
end
|
|
157
|
+
|
|
158
|
+
# Get the fields for a given model, including not just columns (which includes
|
|
159
|
+
# foreign keys), but also associations.
|
|
160
|
+
def self.fields_for(model, exclude_associations: nil)
|
|
161
|
+
foreign_keys = model.reflect_on_all_associations(:belongs_to).map(&:foreign_key)
|
|
162
|
+
|
|
163
|
+
if exclude_associations
|
|
164
|
+
return model.column_names.reject { |c| c.in?(foreign_keys) }
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Add associations in addition to normal columns.
|
|
168
|
+
return model.column_names.reject { |c|
|
|
169
|
+
c.in?(foreign_keys)
|
|
170
|
+
} + model.reflections.map { |association, ref|
|
|
171
|
+
if ref.macro.in?([:has_many, :has_and_belongs_to_many]) &&
|
|
172
|
+
RESTFramework.config.large_reverse_association_tables&.include?(ref.table_name)
|
|
173
|
+
next nil
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
next association
|
|
177
|
+
}.compact
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
|
181
|
+
def self.sub_fields_for(ref)
|
|
182
|
+
model = ref.klass
|
|
183
|
+
|
|
184
|
+
if model
|
|
185
|
+
sub_fields = [model.primary_key].flatten.compact
|
|
186
|
+
|
|
187
|
+
# Preferrably find a database column to use as label.
|
|
188
|
+
if match = LABEL_FIELDS.find { |f| f.in?(model.column_names) }
|
|
189
|
+
return sub_fields + [match]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# Otherwise, find a method.
|
|
193
|
+
if match = LABEL_FIELDS.find { |f| model.method_defined?(f) }
|
|
194
|
+
return sub_fields + [match]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
return sub_fields
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
return ["id", "name"]
|
|
201
|
+
end
|
|
153
202
|
end
|
data/lib/rest_framework.rb
CHANGED
|
@@ -13,6 +13,10 @@ module RESTFramework
|
|
|
13
13
|
RRF_BUILTIN_ACTIONS = {
|
|
14
14
|
options: :options,
|
|
15
15
|
}.freeze
|
|
16
|
+
RRF_BUILTIN_BULK_ACTIONS = {
|
|
17
|
+
update_all: [:put, :patch].freeze,
|
|
18
|
+
destroy_all: :delete,
|
|
19
|
+
}.freeze
|
|
16
20
|
|
|
17
21
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
|
18
22
|
# APIs to be defined to behave differently.
|
|
@@ -24,11 +28,25 @@ module RESTFramework
|
|
|
24
28
|
# in:
|
|
25
29
|
# - Model delegation, for the helper methods to be defined dynamically.
|
|
26
30
|
# - Websockets, for `::Channel` class to be defined dynamically.
|
|
27
|
-
# - Controller configuration
|
|
31
|
+
# - Controller configuration freezing.
|
|
28
32
|
attr_accessor :disable_auto_finalize
|
|
29
33
|
|
|
30
34
|
# Freeze configuration attributes during finalization to prevent accidental mutation.
|
|
31
35
|
attr_accessor :freeze_config
|
|
36
|
+
|
|
37
|
+
# Specify reverse association tables that are typically very large, andd therefore should not be
|
|
38
|
+
# added to fields by default.
|
|
39
|
+
attr_accessor :large_reverse_association_tables
|
|
40
|
+
|
|
41
|
+
# Whether the backtrace should be shown in rescued errors.
|
|
42
|
+
attr_accessor :show_backtrace
|
|
43
|
+
|
|
44
|
+
# Option to disable `rescue_from` on the controller mixins.
|
|
45
|
+
attr_accessor :disable_rescue_from
|
|
46
|
+
|
|
47
|
+
def initialize
|
|
48
|
+
self.show_backtrace = Rails.env.development?
|
|
49
|
+
end
|
|
32
50
|
end
|
|
33
51
|
|
|
34
52
|
def self.config
|
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.7.
|
|
4
|
+
version: 0.7.6
|
|
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-01-
|
|
11
|
+
date: 2023-01-15 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rails
|
|
@@ -42,6 +42,7 @@ files:
|
|
|
42
42
|
- lib/rest_framework.rb
|
|
43
43
|
- lib/rest_framework/controller_mixins.rb
|
|
44
44
|
- lib/rest_framework/controller_mixins/base.rb
|
|
45
|
+
- lib/rest_framework/controller_mixins/bulk.rb
|
|
45
46
|
- lib/rest_framework/controller_mixins/models.rb
|
|
46
47
|
- lib/rest_framework/engine.rb
|
|
47
48
|
- lib/rest_framework/errors.rb
|