standardapi 6.0.0.32 → 7.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|