standardapi 6.0.0.32 → 7.1.0
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/README.md +80 -58
- data/lib/standard_api/access_control_list.rb +148 -0
- data/lib/standard_api/controller.rb +116 -27
- data/lib/standard_api/helpers.rb +17 -10
- data/lib/standard_api/includes.rb +9 -0
- data/lib/standard_api/middleware/query_encoding.rb +3 -3
- data/lib/standard_api/middleware.rb +5 -0
- data/lib/standard_api/railtie.rb +30 -2
- data/lib/standard_api/route_helpers.rb +64 -14
- data/lib/standard_api/test_case/calculate_tests.rb +15 -8
- data/lib/standard_api/test_case/create_tests.rb +7 -9
- data/lib/standard_api/test_case/destroy_tests.rb +19 -7
- data/lib/standard_api/test_case/index_tests.rb +10 -6
- data/lib/standard_api/test_case/schema_tests.rb +7 -1
- data/lib/standard_api/test_case/show_tests.rb +8 -7
- data/lib/standard_api/test_case/update_tests.rb +15 -15
- data/lib/standard_api/test_case.rb +13 -3
- data/lib/standard_api/version.rb +1 -1
- data/lib/standard_api/views/application/_record.json.jbuilder +18 -17
- data/lib/standard_api/views/application/_record.streamer +40 -37
- data/lib/standard_api/views/application/_schema.json.jbuilder +20 -8
- data/lib/standard_api/views/application/_schema.streamer +22 -8
- data/lib/standard_api/views/application/new.streamer +1 -1
- data/lib/standard_api.rb +5 -0
- data/test/standard_api/caching_test.rb +14 -4
- data/test/standard_api/controller/include_test.rb +107 -0
- data/test/standard_api/controller/subresource_test.rb +157 -0
- data/test/standard_api/helpers_test.rb +34 -17
- data/test/standard_api/nested_attributes/belongs_to_test.rb +71 -0
- data/test/standard_api/nested_attributes/has_and_belongs_to_many_test.rb +70 -0
- data/test/standard_api/nested_attributes/has_many_test.rb +85 -0
- data/test/standard_api/nested_attributes/has_one_test.rb +71 -0
- data/test/standard_api/route_helpers_test.rb +56 -0
- data/test/standard_api/standard_api_test.rb +182 -44
- data/test/standard_api/test_app/app/controllers/acl/account_acl.rb +15 -0
- data/test/standard_api/test_app/app/controllers/acl/camera_acl.rb +7 -0
- data/test/standard_api/test_app/app/controllers/acl/photo_acl.rb +13 -0
- data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +33 -0
- data/test/standard_api/test_app/app/controllers/acl/reference_acl.rb +7 -0
- data/test/standard_api/test_app/controllers.rb +28 -43
- data/test/standard_api/test_app/models.rb +76 -7
- data/test/standard_api/test_app/test/factories.rb +7 -3
- data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +1 -0
- data/test/standard_api/test_app/views/photos/_photo.streamer +2 -1
- data/test/standard_api/test_app/views/sessions/create.json.jbuilder +1 -0
- data/test/standard_api/test_app/views/sessions/create.streamer +3 -0
- data/test/standard_api/test_app.rb +12 -1
- data/test/standard_api/test_helper.rb +21 -0
- metadata +59 -16
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 88dd56c94d20d8649c5f755509be1aee880f7dff0133602a4fe46c0408c4e65b
|
4
|
+
data.tar.gz: 3f14301f672ac171f9e2a39620de9f1913300572164978857ea12fcb1bb008de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e3c282a6b8898d1c1451e381dea4cf57c3cd58b65bccfba6a0b5f9d302242287bb233dd43b4a078c51f1711a32a449d47c157626a92cc752939d8040ef704534
|
7
|
+
data.tar.gz: 56d90b5dc3d5d04924b7bd5e5fe6a98cfe9cf4f6fa79d30c933abff3e5c6f6b69a6718a699bb5199183f05372f99d8038f99dc7e5ff1b011dca4da978ee4d5ae
|
data/README.md
CHANGED
@@ -1,36 +1,27 @@
|
|
1
1
|
# StandardAPI
|
2
2
|
|
3
|
-
StandardAPI makes it easy to
|
3
|
+
StandardAPI makes it easy to expose a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
|
4
4
|
interface to your Rails models.
|
5
5
|
|
6
6
|
# Installation
|
7
7
|
|
8
8
|
gem install standardapi
|
9
9
|
|
10
|
-
In your Gemfile
|
10
|
+
In your `Gemfile`:
|
11
11
|
|
12
12
|
gem 'standardapi', require: 'standard_api'
|
13
13
|
|
14
|
-
|
14
|
+
Optionally in `config/application.rb`:
|
15
15
|
|
16
|
-
|
17
|
-
|
18
|
-
require 'rails/all'
|
19
|
-
require 'standard_api/middleware/query_encoding'
|
20
|
-
|
21
|
-
# Require the gems listed in Gemfile, including any gems
|
22
|
-
# you've limited to :test, :development, or :production.
|
23
|
-
Bundler.require(*Rails.groups)
|
24
|
-
|
25
|
-
module Tester
|
16
|
+
module MyApplication
|
26
17
|
class Application < Rails::Application
|
27
18
|
# Initialize configuration defaults for originally generated Rails version.
|
28
|
-
config.load_defaults
|
19
|
+
config.load_defaults 7.0
|
29
20
|
|
30
|
-
#
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
21
|
+
# QueryEncoding middleware intercepts and parses the query string
|
22
|
+
# as MessagePack if the `Query-Encoding` header is set to `application/msgpack`
|
23
|
+
# which allows GET request with types as opposed to all values being interpeted
|
24
|
+
# as strings
|
34
25
|
config.middleware.insert_after Rack::MethodOverride, StandardAPI::Middleware::QueryEncoding
|
35
26
|
end
|
36
27
|
end
|
@@ -41,66 +32,96 @@ StandardAPI is a module that can be included into any controller to expose a API
|
|
41
32
|
for. Alternatly, it can be included into `ApplicationController`, giving all
|
42
33
|
inherited controllers an exposed API.
|
43
34
|
|
44
|
-
class ApplicationController < ActiveController::Base
|
45
|
-
include StandardAPI::Controller
|
46
|
-
|
47
|
-
end
|
48
|
-
|
49
|
-
By default any paramaters passed to update and create are whitelisted with by
|
50
|
-
the method named after the model the controller represents. For example, the
|
51
|
-
following will only allow the `caption` attribute of the `Photo` model to be
|
52
|
-
updated.
|
53
|
-
|
54
35
|
class PhotosController < ApplicationController
|
55
36
|
include StandardAPI
|
56
37
|
|
38
|
+
# Allowed paramaters
|
39
|
+
# By default any paramaters passed to update and create are whitelisted by
|
40
|
+
# the method named after the model the controller represents. For example,
|
41
|
+
# the following will only allow the `caption` attribute of the `Photo`
|
42
|
+
# model to be set on update or create.
|
57
43
|
def photo_params
|
58
44
|
[:caption]
|
59
45
|
end
|
46
|
+
|
47
|
+
# Allowed orderings
|
48
|
+
# The ordering is whitelisted as well, you will mostly likely want to
|
49
|
+
# ensure indexes have been created on these columns. In this example the
|
50
|
+
# response can be ordered by any permutation of `id`, `created_at`, and
|
51
|
+
# `updated_at`.
|
52
|
+
def photo_orders
|
53
|
+
[:id, :created_at, :updated_at]
|
54
|
+
end
|
55
|
+
|
56
|
+
# Allowed includes
|
57
|
+
# Similarly, the includes (including of relationships in the reponse) are
|
58
|
+
# whitelisted. Note how includes can also support nested includes. In this
|
59
|
+
# case when including the author, the photos that the author took can also
|
60
|
+
# be included.
|
61
|
+
def photo_includes
|
62
|
+
{ author: [:photos] }
|
63
|
+
end
|
60
64
|
end
|
61
65
|
|
62
|
-
|
63
|
-
method can be overridden. It simply returns a set of `StrongParameters`.
|
66
|
+
##### Access Control List
|
64
67
|
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
[:comment]
|
73
|
-
end
|
74
|
-
end
|
68
|
+
For greater control of the allowed paramaters and nesting of paramaters
|
69
|
+
`StandardAPI::AccessControlList` is available. To use it include it in your base
|
70
|
+
controller:
|
71
|
+
|
72
|
+
class ApplicationController
|
73
|
+
include StandardAPI::Control
|
74
|
+
include StandardAPI::AccessControlList
|
75
75
|
end
|
76
76
|
|
77
|
-
|
78
|
-
is whitelisted as well.
|
77
|
+
Then create an ACL file for each model you want in `app/controllers/acl`.
|
79
78
|
|
80
|
-
|
79
|
+
Taking the above example we would remove the `photo_*` methods and create the
|
80
|
+
following files:
|
81
81
|
|
82
|
-
|
83
|
-
including StandardAPI
|
82
|
+
`app/controllers/acl/photo_acl.rb`:
|
84
83
|
|
85
|
-
|
86
|
-
|
87
|
-
|
84
|
+
module PhotoACL
|
85
|
+
# Allowed attributes
|
86
|
+
def attributes
|
87
|
+
[ :caption ]
|
88
88
|
end
|
89
|
-
|
90
|
-
# Allowed
|
91
|
-
def
|
92
|
-
[
|
89
|
+
|
90
|
+
# Allowed saving / creating nested attributes
|
91
|
+
def nested
|
92
|
+
[ :camera ]
|
93
93
|
end
|
94
|
-
|
94
|
+
|
95
|
+
# Allowed orders
|
96
|
+
def orders
|
97
|
+
[ :id, :created_at, :updated_at ]
|
98
|
+
end
|
99
|
+
|
95
100
|
# Allowed includes
|
96
|
-
def
|
97
|
-
|
101
|
+
def includes
|
102
|
+
[ :author ]
|
98
103
|
end
|
104
|
+
end
|
99
105
|
|
106
|
+
`app/controllers/acl/author_acl.rb`:
|
107
|
+
|
108
|
+
module AuthorACL
|
109
|
+
def includes
|
110
|
+
[ :photos ]
|
111
|
+
end
|
100
112
|
end
|
101
113
|
|
102
|
-
|
103
|
-
|
114
|
+
All of these methods are optional and will be included in ApplicationController
|
115
|
+
for StandardAPI to determine allowed attributes, nested attributes, orders and
|
116
|
+
includes.
|
117
|
+
|
118
|
+
`includes` now returns a shallow Array, StandardAPI can how determine including
|
119
|
+
an `author` and the author's `photos` is allowed by looking at what includes are
|
120
|
+
allowed on photo and author.
|
121
|
+
|
122
|
+
The `nested` function tells StandardAPI what relations on `Photo` are allowed to
|
123
|
+
be set with the API and will determine what attributes are allowed by looking
|
124
|
+
for a `camera_acl` file.
|
104
125
|
|
105
126
|
# API Usage
|
106
127
|
Resources can be queried via REST style end points
|
@@ -208,7 +229,7 @@ And example contoller and it's tests.
|
|
208
229
|
# The mask is then applyed to all actions when querring ActiveRecord
|
209
230
|
# Will only allow photos that have id one. For more on the syntax see
|
210
231
|
# the activerecord-filter gem.
|
211
|
-
def
|
232
|
+
def mask_for(table_name)
|
212
233
|
{ id: 1 }
|
213
234
|
end
|
214
235
|
|
@@ -231,3 +252,4 @@ StandardAPI Resource Interface
|
|
231
252
|
| `/models?where[id][]=1&where[id][]=2` | `{ where: { id: [1,2] } }` | `SELECT * FROM models WHERE id IN (1, 2)` | `[{ id: 1 }, { id: 2 }]` |
|
232
253
|
|
233
254
|
|
255
|
+
|
@@ -0,0 +1,148 @@
|
|
1
|
+
module StandardAPI
|
2
|
+
module AccessControlList
|
3
|
+
|
4
|
+
def self.traverse(path, prefix: nil, &block)
|
5
|
+
path.children.each do |child|
|
6
|
+
if child.file? && child.basename('.rb').to_s.ends_with?('_acl')
|
7
|
+
block.call([prefix, child.basename('.rb').to_s].compact.join('/'))
|
8
|
+
elsif child.directory?
|
9
|
+
traverse(child, prefix: [prefix, child.basename.to_s].compact.join('/'), &block)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.included(application_controller)
|
15
|
+
acl_dir = Rails.application.root.join('app', 'controllers', 'acl')
|
16
|
+
return if !acl_dir.exist?
|
17
|
+
|
18
|
+
traverse(acl_dir) do |child|
|
19
|
+
mod = child.classify.constantize
|
20
|
+
prefix = child.delete_suffix('_acl').gsub('/', '_')
|
21
|
+
|
22
|
+
[:orders, :includes, :attributes].each do |m|
|
23
|
+
next if !mod.instance_methods.include?(m)
|
24
|
+
mod.send :alias_method, "#{prefix}_#{m}".to_sym, m
|
25
|
+
mod.send :remove_method, m
|
26
|
+
end
|
27
|
+
|
28
|
+
if mod.instance_methods.include?(:nested)
|
29
|
+
mod.send :alias_method, "nested_#{prefix}_attributes".to_sym, :nested
|
30
|
+
mod.send :remove_method, :nested
|
31
|
+
end
|
32
|
+
|
33
|
+
if mod.instance_methods.include?(:filter)
|
34
|
+
mod.send :alias_method, "filter_#{prefix}_params".to_sym, :filter
|
35
|
+
mod.send :remove_method, :filter
|
36
|
+
end
|
37
|
+
|
38
|
+
application_controller.include mod
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def model_orders
|
43
|
+
if self.respond_to?("#{model.model_name.singular}_orders", true)
|
44
|
+
self.send("#{model.model_name.singular}_orders")
|
45
|
+
else
|
46
|
+
[]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def model_params
|
51
|
+
if self.respond_to?("filter_#{model_name(model)}_params", true)
|
52
|
+
self.send("filter_#{model_name(model)}_params", params[model_name(model)], id: params[:id])
|
53
|
+
else
|
54
|
+
filter_model_params(params[model_name(model)], model.base_class)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def filter_model_params(model_params, model, id: nil, allow_id: nil)
|
59
|
+
permitted_params = if model_params && self.respond_to?("#{model_name(model)}_attributes", true)
|
60
|
+
permits = self.send("#{model_name(model)}_attributes")
|
61
|
+
|
62
|
+
allow_id ? model_params.permit(permits, :id) : model_params.permit(permits)
|
63
|
+
else
|
64
|
+
ActionController::Parameters.new
|
65
|
+
end
|
66
|
+
|
67
|
+
if self.respond_to?("nested_#{model_name(model)}_attributes", true)
|
68
|
+
self.send("nested_#{model_name(model)}_attributes").each do |relation|
|
69
|
+
relation = model.reflect_on_association(relation)
|
70
|
+
attributes_key = "#{relation.name}"
|
71
|
+
|
72
|
+
if model_params.has_key?(attributes_key)
|
73
|
+
filter_method = "filter_#{relation.klass.base_class.model_name.singular}_params"
|
74
|
+
if model_params[attributes_key].nil?
|
75
|
+
permitted_params[attributes_key] = nil
|
76
|
+
elsif model_params[attributes_key].is_a?(Array) && model_params[attributes_key].all? { |a| a.keys.map(&:to_sym) == [:id] }
|
77
|
+
permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params[attributes_key].map{|a| a['id']}
|
78
|
+
elsif self.respond_to?(filter_method, true)
|
79
|
+
permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
|
80
|
+
models = relation.klass.find(model_params[attributes_key].map { |i| i['id'] }.compact)
|
81
|
+
model_params[attributes_key].map { |i|
|
82
|
+
i_params = self.send(filter_method, i, allow_id: true)
|
83
|
+
if i_params['id']
|
84
|
+
r = models.find { |r| r.id == i_params['id'] }
|
85
|
+
r.assign_attributes(i_params)
|
86
|
+
r
|
87
|
+
else
|
88
|
+
relation.klass.new(i_params)
|
89
|
+
end
|
90
|
+
}
|
91
|
+
else
|
92
|
+
i_params = self.send(filter_method, model_params[attributes_key], allow_id: true)
|
93
|
+
if i_params['id']
|
94
|
+
r = relation.klass.find(i_params['id'])
|
95
|
+
r.assign_attributes(i_params)
|
96
|
+
r
|
97
|
+
else
|
98
|
+
relation.klass.new(i_params)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
else
|
102
|
+
permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
|
103
|
+
models = relation.klass.find(model_params[attributes_key].map { |i| i['id'] }.compact)
|
104
|
+
model_params[attributes_key].map { |i|
|
105
|
+
i_params = filter_model_params(i, relation.klass.base_class, allow_id: true)
|
106
|
+
if i_params['id']
|
107
|
+
r = models.find { |r| r.id == i_params['id'] }
|
108
|
+
r.assign_attributes(i_params)
|
109
|
+
r
|
110
|
+
else
|
111
|
+
relation.klass.new(i_params)
|
112
|
+
end
|
113
|
+
}
|
114
|
+
else
|
115
|
+
i_params = filter_model_params(model_params[attributes_key], relation.klass.base_class, allow_id: true)
|
116
|
+
if i_params['id']
|
117
|
+
r = relation.klass.find(i_params['id'])
|
118
|
+
r.assign_attributes(i_params)
|
119
|
+
r
|
120
|
+
else
|
121
|
+
relation.klass.new(i_params)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
elsif relation.collection? && model_params.has_key?("#{relation.name.to_s.singularize}_ids")
|
126
|
+
permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params["#{relation.name.to_s.singularize}_ids"]
|
127
|
+
elsif model_params.has_key?(relation.foreign_key)
|
128
|
+
permitted_params[relation.foreign_key] = model_params[relation.foreign_key]
|
129
|
+
permitted_params[relation.foreign_type] = model_params[relation.foreign_type] if relation.polymorphic?
|
130
|
+
end
|
131
|
+
|
132
|
+
permitted_params.permit!
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
permitted_params
|
137
|
+
end
|
138
|
+
|
139
|
+
def model_name(model)
|
140
|
+
if model.model_name.singular.starts_with?('habtm_')
|
141
|
+
model.reflect_on_all_associations.map { |a| a.klass.base_class.model_name.singular }.sort.join('_')
|
142
|
+
else
|
143
|
+
model.model_name.singular
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
end
|
148
|
+
end
|
@@ -1,12 +1,13 @@
|
|
1
1
|
module StandardAPI
|
2
2
|
module Controller
|
3
3
|
|
4
|
-
delegate :preloadables, to: :helpers
|
4
|
+
delegate :preloadables, :model_partial, to: :helpers
|
5
5
|
|
6
6
|
def self.included(klass)
|
7
7
|
klass.helper_method :includes, :orders, :model, :models, :resource_limit,
|
8
8
|
:default_limit
|
9
9
|
klass.before_action :set_standardapi_headers
|
10
|
+
klass.before_action :includes, except: [:destroy, :add_resource, :remove_resource]
|
10
11
|
klass.rescue_from StandardAPI::ParameterMissing, with: :bad_request
|
11
12
|
klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request
|
12
13
|
klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
|
@@ -73,7 +74,7 @@ module StandardAPI
|
|
73
74
|
end
|
74
75
|
else
|
75
76
|
if request.format == :html
|
76
|
-
render :
|
77
|
+
render :new, status: :bad_request
|
77
78
|
else
|
78
79
|
render :show, status: :bad_request
|
79
80
|
end
|
@@ -96,45 +97,97 @@ module StandardAPI
|
|
96
97
|
render :show, status: :ok
|
97
98
|
end
|
98
99
|
else
|
99
|
-
|
100
|
+
if request.format == :html
|
101
|
+
render :edit, status: :bad_request
|
102
|
+
else
|
103
|
+
render :show, status: :bad_request
|
104
|
+
end
|
100
105
|
end
|
101
106
|
end
|
102
107
|
|
103
108
|
def destroy
|
104
|
-
resources.find(params[:id])
|
109
|
+
records = resources.find(params[:id].split(','))
|
110
|
+
model.transaction { records.each(&:destroy!) }
|
111
|
+
|
105
112
|
head :no_content
|
106
113
|
end
|
107
114
|
|
108
115
|
def remove_resource
|
109
116
|
resource = resources.find(params[:id])
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
117
|
+
association = resource.association(params[:relationship])
|
118
|
+
|
119
|
+
result = case association
|
120
|
+
when ActiveRecord::Associations::CollectionAssociation
|
121
|
+
association.delete(association.klass.find(params[:resource_id]))
|
122
|
+
when ActiveRecord::Associations::SingularAssociation
|
123
|
+
if resource.send(params[:relationship])&.id&.to_s == params[:resource_id]
|
124
|
+
resource.update(params[:relationship] => nil)
|
125
|
+
end
|
118
126
|
end
|
127
|
+
head result ? :no_content : :not_found
|
119
128
|
end
|
120
129
|
|
121
130
|
def add_resource
|
122
131
|
resource = resources.find(params[:id])
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
132
|
+
association = resource.association(params[:relationship])
|
133
|
+
subresource = association.klass.find(params[:resource_id])
|
134
|
+
|
135
|
+
result = case association
|
136
|
+
when ActiveRecord::Associations::CollectionAssociation
|
137
|
+
association.concat(subresource)
|
138
|
+
when ActiveRecord::Associations::SingularAssociation
|
139
|
+
resource.update(params[:relationship] => subresource)
|
140
|
+
end
|
141
|
+
head result ? :created : :bad_request
|
142
|
+
rescue ActiveRecord::RecordNotUnique
|
143
|
+
render json: {errors: [
|
144
|
+
"Relationship between #{resource.class.name} and #{subresource.class.name} violates unique constraints"
|
145
|
+
]}, status: :bad_request
|
146
|
+
end
|
147
|
+
|
148
|
+
def create_resource
|
149
|
+
resource = resources.find(params[:id])
|
150
|
+
association = resource.association(params[:relationship])
|
151
|
+
|
152
|
+
subresource_params = if self.respond_to?("filter_#{model_name(association.klass)}_params", true)
|
153
|
+
self.send("filter_#{model_name(association.klass)}_params", params[model_name(association.klass)], id: params[:id])
|
154
|
+
elsif self.respond_to?("#{association.klass.model_name.singular}_params", true)
|
155
|
+
params.require(association.klass.model_name.singular).permit(self.send("#{association.klass.model_name.singular}_params"))
|
156
|
+
elsif self.respond_to?("filter_model_params", true)
|
157
|
+
filter_model_params(params[model_name(association.klass)], association.klass.base_class)
|
129
158
|
else
|
130
|
-
|
159
|
+
ActionController::Parameters.new
|
160
|
+
end
|
161
|
+
|
162
|
+
subresource = association.klass.new(subresource_params)
|
163
|
+
|
164
|
+
result = case association
|
165
|
+
when ActiveRecord::Associations::CollectionAssociation
|
166
|
+
association.concat(subresource)
|
167
|
+
when ActiveRecord::Associations::SingularAssociation
|
168
|
+
resource.update(params[:relationship] => subresource)
|
131
169
|
end
|
132
170
|
|
171
|
+
partial = model_partial(subresource)
|
172
|
+
partial_record_name = partial.split('/').last.to_sym
|
173
|
+
if result
|
174
|
+
render partial: partial, locals: {partial_record_name => subresource}, status: :created
|
175
|
+
else
|
176
|
+
render partial: partial, locals: {partial_record_name => subresource}, status: :bad_request
|
177
|
+
end
|
133
178
|
end
|
134
179
|
|
180
|
+
def mask
|
181
|
+
@mask ||= Hash.new do |hash, key|
|
182
|
+
hash[key] = mask_for(key)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
135
186
|
# Override if you want to support masking
|
136
|
-
def
|
137
|
-
|
187
|
+
def mask_for(table_name)
|
188
|
+
# case table_name
|
189
|
+
# when 'accounts'
|
190
|
+
# end
|
138
191
|
end
|
139
192
|
|
140
193
|
module ClassMethods
|
@@ -157,7 +210,11 @@ module StandardAPI
|
|
157
210
|
end
|
158
211
|
|
159
212
|
def model
|
160
|
-
|
213
|
+
if action_name&.end_with?('_resource')
|
214
|
+
self.class.model.reflect_on_association(params[:relationship]).klass
|
215
|
+
else
|
216
|
+
self.class.model
|
217
|
+
end
|
161
218
|
end
|
162
219
|
|
163
220
|
def models
|
@@ -189,7 +246,7 @@ module StandardAPI
|
|
189
246
|
if self.respond_to?("#{model.model_name.singular}_params", true)
|
190
247
|
params.require(model.model_name.singular).permit(self.send("#{model.model_name.singular}_params"))
|
191
248
|
else
|
192
|
-
|
249
|
+
ActionController::Parameters.new
|
193
250
|
end
|
194
251
|
end
|
195
252
|
|
@@ -207,7 +264,7 @@ module StandardAPI
|
|
207
264
|
end
|
208
265
|
|
209
266
|
def resources
|
210
|
-
query = model.filter(params['where']).filter(
|
267
|
+
query = self.class.model.filter(params['where']).filter(mask[self.class.model.table_name.to_sym])
|
211
268
|
|
212
269
|
if params[:distinct_on]
|
213
270
|
query = query.distinct_on(params[:distinct_on])
|
@@ -225,9 +282,29 @@ module StandardAPI
|
|
225
282
|
|
226
283
|
query
|
227
284
|
end
|
285
|
+
|
286
|
+
def nested_includes(model, attributes)
|
287
|
+
includes = {}
|
288
|
+
attributes&.each do |key, value|
|
289
|
+
if association = model.reflect_on_association(key)
|
290
|
+
includes[key] = nested_includes(association.klass, value)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
includes
|
294
|
+
end
|
228
295
|
|
229
296
|
def includes
|
230
|
-
@includes ||=
|
297
|
+
@includes ||= if params[:include]
|
298
|
+
StandardAPI::Includes.sanitize(params[:include], model_includes)
|
299
|
+
else
|
300
|
+
{}
|
301
|
+
end
|
302
|
+
|
303
|
+
if (action_name == 'create' || action_name == 'update') && model && params.has_key?(model.model_name.singular)
|
304
|
+
@includes.reverse_merge!(nested_includes(model, params[model.model_name.singular].to_unsafe_h))
|
305
|
+
end
|
306
|
+
|
307
|
+
@includes
|
231
308
|
end
|
232
309
|
|
233
310
|
def required_orders
|
@@ -309,7 +386,19 @@ module StandardAPI
|
|
309
386
|
@selects = []
|
310
387
|
@selects << params[:group_by] if params[:group_by]
|
311
388
|
Array(params[:select]).each do |select|
|
312
|
-
select.each do |func,
|
389
|
+
select.each do |func, value|
|
390
|
+
distinct = false
|
391
|
+
|
392
|
+
column = case value
|
393
|
+
when ActionController::Parameters
|
394
|
+
# TODO: Add support for other aggregate expressions
|
395
|
+
# https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES
|
396
|
+
distinct = !value[:distinct].nil?
|
397
|
+
value[:distinct]
|
398
|
+
else
|
399
|
+
value
|
400
|
+
end
|
401
|
+
|
313
402
|
if (parts = column.split(".")).length > 1
|
314
403
|
@model = parts[0].singularize.camelize.constantize
|
315
404
|
column = parts[1]
|
@@ -318,7 +407,7 @@ module StandardAPI
|
|
318
407
|
column = column == '*' ? Arel.star : column.to_sym
|
319
408
|
if functions.include?(func.to_s.downcase)
|
320
409
|
node = (defined?(@model) ? @model : model).arel_table[column].send(func)
|
321
|
-
node.distinct =
|
410
|
+
node.distinct = distinct
|
322
411
|
@selects << node
|
323
412
|
end
|
324
413
|
end
|
data/lib/standard_api/helpers.rb
CHANGED
@@ -1,5 +1,11 @@
|
|
1
1
|
module StandardAPI
|
2
2
|
module Helpers
|
3
|
+
|
4
|
+
def serialize_attribute(json, record, name, type)
|
5
|
+
value = record.send(name)
|
6
|
+
|
7
|
+
json.set! name, type == :binary ? value&.unpack1('H*') : value
|
8
|
+
end
|
3
9
|
|
4
10
|
def preloadables(record, includes)
|
5
11
|
preloads = {}
|
@@ -80,9 +86,10 @@ module StandardAPI
|
|
80
86
|
end
|
81
87
|
end
|
82
88
|
|
83
|
-
def can_cache_relation?(
|
89
|
+
def can_cache_relation?(record, relation, subincludes)
|
90
|
+
return false if record.new_record?
|
84
91
|
cache_columns = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
|
85
|
-
if (cache_columns -
|
92
|
+
if (cache_columns - record.class.column_names).empty?
|
86
93
|
true
|
87
94
|
else
|
88
95
|
false
|
@@ -91,18 +98,18 @@ module StandardAPI
|
|
91
98
|
|
92
99
|
def association_cache_key(record, relation, subincludes)
|
93
100
|
timestamp = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
|
94
|
-
timestamp.map! { |col| record.send(col) }
|
101
|
+
timestamp = (timestamp & record.class.column_names).map! { |col| record.send(col) }
|
95
102
|
timestamp = timestamp.max
|
96
103
|
|
97
104
|
case association = record.class.reflect_on_association(relation)
|
98
105
|
when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
|
99
|
-
"#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.
|
106
|
+
"#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_fs(record.cache_timestamp_format)}"
|
100
107
|
when ActiveRecord::Reflection::BelongsToReflection
|
101
108
|
klass = association.options[:polymorphic] ? record.send(association.foreign_type).constantize : association.klass
|
102
109
|
if subincludes.empty?
|
103
|
-
"#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.
|
110
|
+
"#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.to_fs(klass.cache_timestamp_format)}"
|
104
111
|
else
|
105
|
-
"#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.
|
112
|
+
"#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.to_fs(klass.cache_timestamp_format)}"
|
106
113
|
end
|
107
114
|
else
|
108
115
|
raise ArgumentError, 'Unkown association type'
|
@@ -155,7 +162,9 @@ module StandardAPI
|
|
155
162
|
|
156
163
|
def json_column_type(sql_type)
|
157
164
|
case sql_type
|
158
|
-
when '
|
165
|
+
when 'binary', 'bytea'
|
166
|
+
'binary'
|
167
|
+
when /timestamp(\(\d+\))? without time zone/
|
159
168
|
'datetime'
|
160
169
|
when 'time without time zone'
|
161
170
|
'datetime'
|
@@ -163,9 +172,7 @@ module StandardAPI
|
|
163
172
|
'string'
|
164
173
|
when 'json'
|
165
174
|
'hash'
|
166
|
-
when 'bigint'
|
167
|
-
'integer'
|
168
|
-
when 'integer'
|
175
|
+
when 'smallint', 'bigint', 'integer'
|
169
176
|
'integer'
|
170
177
|
when 'jsonb'
|
171
178
|
'hash'
|
@@ -20,6 +20,15 @@ module StandardAPI
|
|
20
20
|
normalized[k] = case k.to_s
|
21
21
|
when 'when', 'where', 'order'
|
22
22
|
case v
|
23
|
+
when Array
|
24
|
+
v.map do |x|
|
25
|
+
case x
|
26
|
+
when Hash then x.to_h
|
27
|
+
when ActionController::Parameters then x.to_unsafe_h
|
28
|
+
else
|
29
|
+
x
|
30
|
+
end
|
31
|
+
end
|
23
32
|
when Hash then v.to_h
|
24
33
|
when ActionController::Parameters then v.to_unsafe_h
|
25
34
|
end
|