rest_framework 0.8.15 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +149 -128
- data/app/views/rest_framework/_head.html.erb +249 -28
- data/app/views/rest_framework/_html_form.html.erb +55 -3
- data/app/views/rest_framework/_raw_form.html.erb +31 -3
- data/docs/CNAME +1 -0
- data/docs/Gemfile +4 -0
- data/docs/Gemfile.lock +264 -0
- data/docs/_config.yml +17 -0
- data/docs/_guide/1_routers.md +110 -0
- data/docs/_guide/2_controller_mixins.md +293 -0
- data/docs/_guide/3_serializers.md +60 -0
- data/docs/_guide/4_filtering_and_ordering.md +41 -0
- data/docs/_guide/5_pagination.md +21 -0
- data/docs/_includes/anchor_headings.html +144 -0
- data/docs/_includes/head.html +35 -0
- data/docs/_includes/header.html +58 -0
- data/docs/_layouts/default.html +11 -0
- data/docs/assets/css/rest_framework.css +159 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/js/rest_framework.js +132 -0
- data/docs/index.md +133 -0
- data/lib/rest_framework/controller_mixins/base.rb +10 -3
- data/lib/rest_framework/controller_mixins/models.rb +120 -44
- data/lib/rest_framework/filters.rb +7 -9
- data/lib/rest_framework/serializers.rb +35 -12
- data/lib/rest_framework/utils.rb +11 -2
- data/lib/rest_framework/version.rb +4 -1
- data/lib/rest_framework.rb +2 -8
- metadata +22 -6
- data/app/views/rest_framework/_form_routes.html.erb +0 -10
data/docs/index.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
---
|
2
|
+
---
|
3
|
+
|
4
|
+
# Rails REST Framework
|
5
|
+
|
6
|
+
[](https://badge.fury.io/rb/rest_framework)
|
7
|
+
[](https://github.com/gregschmit/rails-rest-framework/actions/workflows/pipeline.yml)
|
8
|
+
[](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
|
9
|
+
[](https://codeclimate.com/github/gregschmit/rails-rest-framework/maintainability)
|
10
|
+
|
11
|
+
A framework for DRY RESTful APIs in Ruby on Rails.
|
12
|
+
|
13
|
+
**The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
|
14
|
+
logic, and routing them can be obnoxious. Building and maintaining features like ordering,
|
15
|
+
filtering, and pagination can be tedious.
|
16
|
+
|
17
|
+
**The Solution**: This framework implements browsable API responses, CRUD actions for your models,
|
18
|
+
and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
|
19
|
+
|
20
|
+
Website/Guide: [rails-rest-framework.com](https://rails-rest-framework.com)
|
21
|
+
|
22
|
+
Demo: [demo.rails-rest-framework.com](https://demo.rails-rest-framework.com)
|
23
|
+
|
24
|
+
Source: [github.com/gregschmit/rails-rest-framework](https://github.com/gregschmit/rails-rest-framework)
|
25
|
+
|
26
|
+
YARD Docs: [rubydoc.info/gems/rest_framework](https://rubydoc.info/gems/rest_framework)
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Add this line to your application's Gemfile:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem 'rest_framework'
|
34
|
+
```
|
35
|
+
|
36
|
+
And then execute:
|
37
|
+
|
38
|
+
```shell
|
39
|
+
$ bundle install
|
40
|
+
```
|
41
|
+
|
42
|
+
Or install it yourself with:
|
43
|
+
|
44
|
+
```shell
|
45
|
+
$ gem install rest_framework
|
46
|
+
```
|
47
|
+
|
48
|
+
## Quick Usage Tutorial
|
49
|
+
|
50
|
+
### Controller Mixins
|
51
|
+
|
52
|
+
To transform a controller into a RESTful controller, you can either include `BaseControllerMixin`,
|
53
|
+
`ReadOnlyModelControllerMixin`, or `ModelControllerMixin`. `BaseControllerMixin` provides a `root`
|
54
|
+
action and a simple interface for routing arbitrary additional actions:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class ApiController < ApplicationController
|
58
|
+
include RESTFramework::BaseControllerMixin
|
59
|
+
self.extra_actions = {test: [:get]}
|
60
|
+
|
61
|
+
def test
|
62
|
+
render api_response({message: "Test successful!"})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
`ModelControllerMixin` assists with providing the standard model CRUD for your controller.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class Api::MoviesController < ApiController
|
71
|
+
include RESTFramework::ModelControllerMixin
|
72
|
+
|
73
|
+
self.recordset = Movie.where(enabled: true)
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
`ReadOnlyModelControllerMixin` only enables list/show actions, but since we're naming this
|
78
|
+
controller in a way that doesn't make the model obvious, we can set that explicitly:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class Api::ReadOnlyMoviesController < ApiController
|
82
|
+
include RESTFramework::ReadOnlyModelControllerMixin
|
83
|
+
|
84
|
+
self.model = Movie
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
Note that you can also override the `get_recordset` instance method to override the API behavior
|
89
|
+
dynamically per-request.
|
90
|
+
|
91
|
+
### Routing
|
92
|
+
|
93
|
+
You can use Rails' `resource`/`resources` routers to route your API, however if you want
|
94
|
+
`extra_actions` / `extra_member_actions` to be routed automatically, then you can use `rest_route`
|
95
|
+
for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers. You can
|
96
|
+
also use `rest_root` to route the root of your API:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
Rails.application.routes.draw do
|
100
|
+
rest_root :api # will find `api_controller` and route the `root` action to '/api'
|
101
|
+
namespace :api do
|
102
|
+
rest_resources :movies
|
103
|
+
rest_resources :users
|
104
|
+
end
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
Or if you want the API root to be routed to `Api::RootController#root`:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
Rails.application.routes.draw do
|
112
|
+
namespace :api do
|
113
|
+
rest_root # will route `Api::RootController#root` to '/' in this namespace ('/api')
|
114
|
+
rest_resources :movies
|
115
|
+
rest_resources :users
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
## Development/Testing
|
121
|
+
|
122
|
+
After you clone the repository, cd'ing into the directory should create a new gemset if you are
|
123
|
+
using RVM. Then run `bundle install` to install the appropriate gems.
|
124
|
+
|
125
|
+
To run the test suite:
|
126
|
+
|
127
|
+
```shell
|
128
|
+
$ rails test
|
129
|
+
```
|
130
|
+
|
131
|
+
The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via
|
132
|
+
the usual commands. Ensure you run `rails db:setup` before running `rails server` or
|
133
|
+
`rails console`.
|
@@ -8,8 +8,16 @@ require_relative "../utils"
|
|
8
8
|
module RESTFramework::BaseControllerMixin
|
9
9
|
RRF_BASE_CONTROLLER_CONFIG = {
|
10
10
|
filter_pk_from_request_body: true,
|
11
|
-
exclude_body_fields: [
|
12
|
-
|
11
|
+
exclude_body_fields: %w[
|
12
|
+
created_at
|
13
|
+
created_by
|
14
|
+
created_by_id
|
15
|
+
updated_at
|
16
|
+
updated_by
|
17
|
+
updated_by_id
|
18
|
+
_method
|
19
|
+
utf8
|
20
|
+
authenticity_token
|
13
21
|
].freeze,
|
14
22
|
extra_actions: nil,
|
15
23
|
extra_member_actions: nil,
|
@@ -323,7 +331,6 @@ module RESTFramework::BaseControllerMixin
|
|
323
331
|
@json_payload = payload.to_json if self.class.serialize_to_json
|
324
332
|
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
325
333
|
end
|
326
|
-
@template_logo_text ||= "Rails REST Framework"
|
327
334
|
@title ||= self.class.get_title
|
328
335
|
@description ||= self.class.description
|
329
336
|
@route_props, @route_groups = RESTFramework::Utils.get_routes(
|
@@ -3,6 +3,15 @@ require_relative "../filters"
|
|
3
3
|
|
4
4
|
# This module provides the core functionality for controllers based on models.
|
5
5
|
module RESTFramework::BaseModelControllerMixin
|
6
|
+
BASE64_REGEX = /data:(.*);base64,(.*)/
|
7
|
+
BASE64_TRANSLATE = ->(field, value) {
|
8
|
+
_, content_type, payload = value.match(BASE64_REGEX).to_a
|
9
|
+
return {
|
10
|
+
io: StringIO.new(Base64.decode64(payload)),
|
11
|
+
content_type: content_type,
|
12
|
+
filename: "image_#{field}#{Rack::Mime::MIME_TYPES.invert[content_type]}",
|
13
|
+
}
|
14
|
+
}
|
6
15
|
include RESTFramework::BaseControllerMixin
|
7
16
|
|
8
17
|
RRF_BASE_MODEL_CONTROLLER_CONFIG = {
|
@@ -89,20 +98,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
89
98
|
return self.get_model.human_attribute_name(s, default: super)
|
90
99
|
end
|
91
100
|
|
92
|
-
# Get the available fields.
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
def get_fields(input_fields: nil, fallback: true)
|
97
|
-
input_fields ||= self.fields if fallback
|
101
|
+
# Get the available fields. Fallback to this controller's model columns, or an empty array. This
|
102
|
+
# should always return an array of strings.
|
103
|
+
def get_fields(input_fields: nil)
|
104
|
+
input_fields ||= self.fields
|
98
105
|
|
99
106
|
# If fields is a hash, then parse it.
|
100
107
|
if input_fields.is_a?(Hash)
|
101
108
|
return RESTFramework::Utils.parse_fields_hash(
|
102
109
|
input_fields, self.get_model, exclude_associations: self.exclude_associations
|
103
110
|
)
|
104
|
-
elsif !input_fields
|
105
|
-
# Otherwise, if fields is nil
|
111
|
+
elsif !input_fields
|
112
|
+
# Otherwise, if fields is nil, then fallback to columns.
|
106
113
|
model = self.get_model
|
107
114
|
return model ? RESTFramework::Utils.fields_for(
|
108
115
|
model, exclude_associations: self.exclude_associations
|
@@ -148,7 +155,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
148
155
|
end
|
149
156
|
|
150
157
|
# Get metadata about the resource's fields.
|
151
|
-
def
|
158
|
+
def fields_metadata
|
159
|
+
return @_fields_metadata if @_fields_metadata
|
160
|
+
|
152
161
|
# Get metadata sources.
|
153
162
|
model = self.get_model
|
154
163
|
fields = self.get_fields.map(&:to_s)
|
@@ -156,8 +165,14 @@ module RESTFramework::BaseModelControllerMixin
|
|
156
165
|
column_defaults = model.column_defaults
|
157
166
|
reflections = model.reflections
|
158
167
|
attributes = model._default_attributes
|
159
|
-
|
160
|
-
|
168
|
+
readonly_attributes = model.readonly_attributes
|
169
|
+
exclude_body_fields = self.exclude_body_fields.map(&:to_s)
|
170
|
+
rich_text_association_names = model.reflect_on_all_associations(:has_one)
|
171
|
+
.collect(&:name)
|
172
|
+
.select { |n| n.to_s.start_with?("rich_text_") }
|
173
|
+
attachment_reflections = model.attachment_reflections
|
174
|
+
|
175
|
+
return @_fields_metadata = fields.map { |f|
|
161
176
|
# Initialize metadata to make the order consistent.
|
162
177
|
metadata = {
|
163
178
|
type: nil,
|
@@ -173,6 +188,11 @@ module RESTFramework::BaseModelControllerMixin
|
|
173
188
|
metadata[:primary_key] = true
|
174
189
|
end
|
175
190
|
|
191
|
+
# Determine if the field is a read-only attribute.
|
192
|
+
if metadata[:primary_key] || f.in?(readonly_attributes) || f.in?(exclude_body_fields)
|
193
|
+
metadata[:read_only] = true
|
194
|
+
end
|
195
|
+
|
176
196
|
# Determine `type`, `required`, `label`, and `kind` based on schema.
|
177
197
|
if column = columns[f]
|
178
198
|
metadata[:kind] = "column"
|
@@ -249,9 +269,22 @@ module RESTFramework::BaseModelControllerMixin
|
|
249
269
|
}.compact
|
250
270
|
end
|
251
271
|
|
272
|
+
# Determine if this is an ActionText "rich text".
|
273
|
+
if :"rich_text_#{f}".in?(rich_text_association_names)
|
274
|
+
metadata[:kind] = "rich_text"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Determine if this is an ActiveStorage attachment.
|
278
|
+
if ref = attachment_reflections[f]
|
279
|
+
metadata[:kind] = "attachment"
|
280
|
+
metadata[:attachment] = {
|
281
|
+
macro: ref.macro,
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
252
285
|
# Determine if this is just a method.
|
253
|
-
if model.method_defined?(f)
|
254
|
-
metadata[:kind]
|
286
|
+
if !metadata[:kind] && model.method_defined?(f)
|
287
|
+
metadata[:kind] = "method"
|
255
288
|
end
|
256
289
|
|
257
290
|
# Collect validator options into a hash on their type, while also updating `required` based
|
@@ -290,7 +323,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
290
323
|
def get_options_metadata
|
291
324
|
return super.merge(
|
292
325
|
{
|
293
|
-
|
326
|
+
primary_key: self.get_model.primary_key,
|
327
|
+
fields: self.fields_metadata,
|
294
328
|
callbacks: self._process_action_callbacks.as_json,
|
295
329
|
},
|
296
330
|
)
|
@@ -377,9 +411,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
377
411
|
end
|
378
412
|
|
379
413
|
# Get a list of fields, taking into account the current action.
|
380
|
-
def get_fields
|
414
|
+
def get_fields
|
381
415
|
fields = self._get_specific_action_config(:action_fields, :fields)
|
382
|
-
return self.class.get_fields(input_fields: fields
|
416
|
+
return self.class.get_fields(input_fields: fields)
|
383
417
|
end
|
384
418
|
|
385
419
|
# Pass fields to get dynamic metadata based on which fields are available.
|
@@ -387,14 +421,12 @@ module RESTFramework::BaseModelControllerMixin
|
|
387
421
|
return self.class.get_options_metadata
|
388
422
|
end
|
389
423
|
|
390
|
-
# Get a list of find_by fields for the current action.
|
391
|
-
# wants to find by virtual columns.
|
424
|
+
# Get a list of find_by fields for the current action.
|
392
425
|
def get_find_by_fields
|
393
|
-
return self.class.find_by_fields
|
426
|
+
return self.class.find_by_fields
|
394
427
|
end
|
395
428
|
|
396
|
-
# Get a list of parameters allowed for the current action.
|
397
|
-
# columns so arbitrary fields can be submitted if no fields are defined.
|
429
|
+
# Get a list of parameters allowed for the current action.
|
398
430
|
def get_allowed_parameters
|
399
431
|
return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
|
400
432
|
|
@@ -403,35 +435,54 @@ module RESTFramework::BaseModelControllerMixin
|
|
403
435
|
:allowed_parameters,
|
404
436
|
)
|
405
437
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
406
|
-
return @_get_allowed_parameters = nil unless fields = self.get_fields
|
407
438
|
|
408
439
|
# For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
|
409
|
-
|
410
|
-
|
411
|
-
|
440
|
+
variations = []
|
441
|
+
hash_variations = {}
|
442
|
+
reflections = self.class.get_model.reflections
|
443
|
+
@_get_allowed_parameters = self.get_fields.map { |f|
|
412
444
|
f = f.to_s
|
413
|
-
|
445
|
+
|
446
|
+
# ActiveStorage Integration: `has_one_attached`.
|
447
|
+
if reflections.key?("#{f}_attachment")
|
448
|
+
next f
|
449
|
+
end
|
450
|
+
|
451
|
+
# ActiveStorage Integration: `has_many_attached`.
|
452
|
+
if reflections.key?("#{f}_attachments")
|
453
|
+
hash_variations[f] = []
|
454
|
+
next nil
|
455
|
+
end
|
456
|
+
|
457
|
+
# ActionText Integration.
|
458
|
+
if reflections.key?("rich_test_#{f}")
|
459
|
+
next f
|
460
|
+
end
|
461
|
+
|
462
|
+
# Return field if it's not an association.
|
463
|
+
next f unless ref = reflections[f]
|
414
464
|
|
415
465
|
if self.class.permit_id_assignment
|
416
466
|
if ref.collection?
|
417
|
-
|
467
|
+
hash_variations["#{f.singularize}_ids"] = []
|
418
468
|
elsif ref.belongs_to?
|
419
|
-
|
469
|
+
variations << "#{f}_id"
|
420
470
|
end
|
421
471
|
end
|
422
472
|
|
423
473
|
if self.class.permit_nested_attributes_assignment
|
424
474
|
if self.class.allow_all_nested_attributes
|
425
|
-
|
475
|
+
hash_variations["#{f}_attributes"] = {}
|
426
476
|
else
|
427
|
-
|
477
|
+
hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
|
428
478
|
end
|
429
479
|
end
|
430
480
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
@_get_allowed_parameters
|
481
|
+
# Associations are not allowed to be submitted in their bare form.
|
482
|
+
next nil
|
483
|
+
}.compact
|
484
|
+
@_get_allowed_parameters += variations
|
485
|
+
@_get_allowed_parameters << hash_variations
|
435
486
|
return @_get_allowed_parameters
|
436
487
|
end
|
437
488
|
|
@@ -454,14 +505,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
454
505
|
def get_body_params(data: nil)
|
455
506
|
data ||= request.request_parameters
|
456
507
|
|
457
|
-
# Filter the request body
|
458
|
-
|
459
|
-
body_params = if allowed_parameters = self.get_allowed_parameters
|
460
|
-
data = ActionController::Parameters.new(data)
|
461
|
-
data.permit(*allowed_parameters)
|
462
|
-
else
|
463
|
-
data
|
464
|
-
end
|
508
|
+
# Filter the request body with strong params.
|
509
|
+
body_params = ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
|
465
510
|
|
466
511
|
# Filter primary key if configured.
|
467
512
|
if self.class.filter_pk_from_request_body
|
@@ -471,6 +516,27 @@ module RESTFramework::BaseModelControllerMixin
|
|
471
516
|
# Filter fields in `exclude_body_fields`.
|
472
517
|
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
473
518
|
|
519
|
+
# ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
|
520
|
+
#
|
521
|
+
# rubocop:disable Layout/LineLength
|
522
|
+
#
|
523
|
+
# Good example base64 image:
|
524
|
+
# data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=
|
525
|
+
#
|
526
|
+
# rubocop:enable Layout/LineLength
|
527
|
+
self.class.get_model.attachment_reflections.keys.each do |k|
|
528
|
+
next unless (body_params[k].is_a?(String) && body_params[k].match?(BASE64_REGEX)) ||
|
529
|
+
(body_params[k].is_a?(Array) && body_params[k].all? { |v|
|
530
|
+
v.is_a?(String) && v.match?(BASE64_REGEX)
|
531
|
+
})
|
532
|
+
|
533
|
+
if body_params[k].is_a?(Array)
|
534
|
+
body_params[k] = body_params[k].map { |v| BASE64_TRANSLATE.call(k, v) }
|
535
|
+
else
|
536
|
+
body_params[k] = BASE64_TRANSLATE.call(k, body_params[k])
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
474
540
|
return body_params
|
475
541
|
end
|
476
542
|
alias_method :get_create_params, :get_body_params
|
@@ -492,8 +558,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
492
558
|
|
493
559
|
# Get the recordset but with any associations included to avoid N+1 queries.
|
494
560
|
def get_recordset_with_includes
|
495
|
-
reflections = self.class.get_model.reflections
|
496
|
-
associations = self.get_fields
|
561
|
+
reflections = self.class.get_model.reflections
|
562
|
+
associations = self.get_fields.map { |f|
|
563
|
+
if reflections.key?(f)
|
564
|
+
f.to_sym
|
565
|
+
elsif reflections.key?("rich_text_#{f}")
|
566
|
+
:"rich_text_#{f}"
|
567
|
+
elsif reflections.key?("#{f}_attachment")
|
568
|
+
:"#{f}_attachment"
|
569
|
+
elsif reflections.key?("#{f}_attachments")
|
570
|
+
:"#{f}_attachments"
|
571
|
+
end
|
572
|
+
}.compact
|
497
573
|
|
498
574
|
if associations.any?
|
499
575
|
return self.get_recordset.includes(associations)
|
@@ -11,11 +11,10 @@ end
|
|
11
11
|
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
12
12
|
# controller class.
|
13
13
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
14
|
-
# Get a list of filterset fields for the current action.
|
15
|
-
# to try filtering by any query parameter because that could clash with other query parameters.
|
14
|
+
# Get a list of filterset fields for the current action.
|
16
15
|
def _get_fields
|
17
16
|
# Always return a list of strings; `@controller.get_fields` already does this.
|
18
|
-
return @controller.class.filterset_fields&.map(&:to_s) || @controller.get_fields
|
17
|
+
return @controller.class.filterset_fields&.map(&:to_s) || @controller.get_fields
|
19
18
|
end
|
20
19
|
|
21
20
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
@@ -64,8 +63,7 @@ end
|
|
64
63
|
|
65
64
|
# A filter backend which handles ordering of the recordset.
|
66
65
|
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
67
|
-
# Get a list of ordering fields for the current action.
|
68
|
-
# user wants to order by a virtual column.
|
66
|
+
# Get a list of ordering fields for the current action.
|
69
67
|
def _get_fields
|
70
68
|
return @controller.class.ordering_fields&.map(&:to_s) || @controller.get_fields
|
71
69
|
end
|
@@ -88,7 +86,8 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
88
86
|
column = field
|
89
87
|
direction = :asc
|
90
88
|
end
|
91
|
-
|
89
|
+
|
90
|
+
next if !column.in?(fields) && column.split(".").first.in?(fields)
|
92
91
|
|
93
92
|
ordering[column] = direction
|
94
93
|
end
|
@@ -113,15 +112,14 @@ end
|
|
113
112
|
|
114
113
|
# Multi-field text searching on models.
|
115
114
|
class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
116
|
-
# Get a list of search fields for the current action.
|
117
|
-
# common string-like columns by default.
|
115
|
+
# Get a list of search fields for the current action.
|
118
116
|
def _get_fields
|
119
117
|
if search_fields = @controller.class.search_fields
|
120
118
|
return search_fields&.map(&:to_s)
|
121
119
|
end
|
122
120
|
|
123
121
|
columns = @controller.class.get_model.column_names
|
124
|
-
return @controller.get_fields
|
122
|
+
return @controller.get_fields.select { |f|
|
125
123
|
f.in?(RESTFramework.config.search_columns) && f.in?(columns)
|
126
124
|
}
|
127
125
|
end
|
@@ -219,13 +219,18 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
219
219
|
includes = {}
|
220
220
|
methods = []
|
221
221
|
serializer_methods = {}
|
222
|
+
|
223
|
+
column_names = @model.column_names
|
224
|
+
reflections = @model.reflections
|
225
|
+
attachment_reflections = @model.attachment_reflections
|
226
|
+
|
222
227
|
fields.each do |f|
|
223
228
|
field_config = @controller.class.get_field_config(f)
|
224
229
|
next if field_config[:write_only]
|
225
230
|
|
226
|
-
if f.in?(
|
231
|
+
if f.in?(column_names)
|
227
232
|
columns << f
|
228
|
-
elsif ref =
|
233
|
+
elsif ref = reflections[f]
|
229
234
|
sub_columns = []
|
230
235
|
sub_methods = []
|
231
236
|
field_config[:sub_fields].each do |sf|
|
@@ -242,9 +247,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
242
247
|
# If we need to limit the number of serialized association records, then dynamically add a
|
243
248
|
# serializer method to do so.
|
244
249
|
if limit = self._get_associations_limit
|
245
|
-
|
246
|
-
|
247
|
-
self.define_singleton_method(method_name) do |record|
|
250
|
+
serializer_methods[f] = f
|
251
|
+
self.define_singleton_method(f) do |record|
|
248
252
|
next record.send(f).limit(limit).as_json(**sub_config)
|
249
253
|
end
|
250
254
|
else
|
@@ -253,8 +257,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
253
257
|
|
254
258
|
# If we need to include the association count, then add it here.
|
255
259
|
if @controller.class.native_serializer_include_associations_count
|
256
|
-
method_name = "
|
257
|
-
serializer_methods[method_name] =
|
260
|
+
method_name = "#{f}.count"
|
261
|
+
serializer_methods[method_name] = method_name
|
258
262
|
self.define_singleton_method(method_name) do |record|
|
259
263
|
next record.send(f).count
|
260
264
|
end
|
@@ -262,6 +266,24 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
262
266
|
else
|
263
267
|
includes[f] = sub_config
|
264
268
|
end
|
269
|
+
elsif ref = reflections["rich_text_#{f}"]
|
270
|
+
# ActionText Integration: Define rich text serializer method.
|
271
|
+
serializer_methods[f] = f
|
272
|
+
self.define_singleton_method(f) do |record|
|
273
|
+
next record.send(f).to_s
|
274
|
+
end
|
275
|
+
elsif ref = attachment_reflections[f]
|
276
|
+
# ActiveStorage Integration: Define attachment serializer method.
|
277
|
+
serializer_methods[f] = f
|
278
|
+
if ref.macro == :has_one_attached
|
279
|
+
self.define_singleton_method(f) do |record|
|
280
|
+
next record.send(f).attachment&.url
|
281
|
+
end
|
282
|
+
else
|
283
|
+
self.define_singleton_method(f) do |record|
|
284
|
+
next record.send(f).map(&:url)
|
285
|
+
end
|
286
|
+
end
|
265
287
|
elsif @model.method_defined?(f)
|
266
288
|
methods << f
|
267
289
|
end
|
@@ -272,9 +294,10 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
272
294
|
}
|
273
295
|
end
|
274
296
|
|
275
|
-
# Get the raw serializer config
|
276
|
-
#
|
277
|
-
|
297
|
+
# Get the raw serializer config, prior to any adjustments from the request.
|
298
|
+
#
|
299
|
+
# Use `deep_dup` on any class mutables (array, hash, etc) to avoid mutating class state.
|
300
|
+
def get_raw_serializer_config
|
278
301
|
# Return a locally defined serializer config if one is defined.
|
279
302
|
if local_config = self.get_local_native_serializer_config
|
280
303
|
return local_config.deep_dup
|
@@ -286,7 +309,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
286
309
|
end
|
287
310
|
|
288
311
|
# If the config wasn't determined, build a serializer config from controller fields.
|
289
|
-
if @model && fields = @controller&.get_fields
|
312
|
+
if @model && fields = @controller&.get_fields
|
290
313
|
return self._get_controller_serializer_config(fields.deep_dup)
|
291
314
|
end
|
292
315
|
|
@@ -296,7 +319,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
296
319
|
|
297
320
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
298
321
|
def get_serializer_config
|
299
|
-
return filter_from_request(self.
|
322
|
+
return filter_from_request(self.get_raw_serializer_config)
|
300
323
|
end
|
301
324
|
|
302
325
|
# Serialize a single record and merge results of `serializer_methods`.
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -180,7 +180,12 @@ module RESTFramework::Utils
|
|
180
180
|
return model.column_names.reject { |c|
|
181
181
|
c.in?(foreign_keys)
|
182
182
|
} + model.reflections.map { |association, ref|
|
183
|
-
#
|
183
|
+
# Ignore associations for which we have custom integrations.
|
184
|
+
if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob ActionText::RichText))
|
185
|
+
next nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# Exclude user-specified associations.
|
184
189
|
if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
|
185
190
|
next nil
|
186
191
|
end
|
@@ -192,7 +197,11 @@ module RESTFramework::Utils
|
|
192
197
|
end
|
193
198
|
|
194
199
|
next association
|
195
|
-
}.compact
|
200
|
+
}.compact + model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
|
201
|
+
n.to_s.start_with?("rich_text_")
|
202
|
+
}.map { |n|
|
203
|
+
n.to_s.delete_prefix("rich_text_")
|
204
|
+
} + model.attachment_reflections.keys
|
196
205
|
end
|
197
206
|
|
198
207
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
@@ -28,7 +28,10 @@ module RESTFramework
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.stamp_version
|
31
|
-
|
31
|
+
# Only stamp the version if it's not unknown.
|
32
|
+
if RESTFramework::VERSION != "0.unknown"
|
33
|
+
File.write(VERSION_FILEPATH, RESTFramework::VERSION)
|
34
|
+
end
|
32
35
|
end
|
33
36
|
|
34
37
|
def self.unstamp_version
|
data/lib/rest_framework.rb
CHANGED
@@ -21,11 +21,7 @@ module RESTFramework
|
|
21
21
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
22
22
|
# APIs to be defined to behave differently.
|
23
23
|
class Config
|
24
|
-
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES =
|
25
|
-
ActionText::RichText
|
26
|
-
ActiveStorage::Attachment
|
27
|
-
ActiveStorage::Blob
|
28
|
-
).freeze
|
24
|
+
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES = [].freeze
|
29
25
|
DEFAULT_LABEL_FIELDS = %w(name label login title email username url).freeze
|
30
26
|
DEFAULT_SEARCH_COLUMNS = DEFAULT_LABEL_FIELDS + %w(description note).freeze
|
31
27
|
|
@@ -78,9 +74,7 @@ module RESTFramework
|
|
78
74
|
end
|
79
75
|
|
80
76
|
def self.features
|
81
|
-
return @features ||= {
|
82
|
-
html_forms: false,
|
83
|
-
}
|
77
|
+
return @features ||= {}
|
84
78
|
end
|
85
79
|
end
|
86
80
|
|