rest_framework 0.7.4 → 0.7.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/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
|