standardapi 6.1.0 → 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 +40 -6
- data/lib/standard_api/controller.rb +96 -28
- data/lib/standard_api/helpers.rb +13 -7
- data/lib/standard_api/middleware.rb +5 -0
- data/lib/standard_api/railtie.rb +17 -0
- data/lib/standard_api/route_helpers.rb +59 -9
- data/lib/standard_api/test_case/calculate_tests.rb +7 -6
- data/lib/standard_api/test_case/destroy_tests.rb +19 -7
- data/lib/standard_api/test_case/index_tests.rb +7 -13
- data/lib/standard_api/test_case/show_tests.rb +7 -7
- data/lib/standard_api/test_case/update_tests.rb +7 -6
- data/lib/standard_api/version.rb +1 -1
- data/lib/standard_api/views/application/_record.json.jbuilder +4 -3
- data/lib/standard_api/views/application/_record.streamer +4 -3
- 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.rb +1 -0
- data/test/standard_api/caching_test.rb +2 -2
- 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 +9 -8
- 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 +80 -50
- 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 +7 -1
- data/test/standard_api/test_app/controllers.rb +17 -0
- data/test/standard_api/test_app/models.rb +59 -2
- data/test/standard_api/test_app/test/factories.rb +3 -0
- 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 +9 -0
- metadata +52 -13
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
|
+
|
@@ -56,7 +56,7 @@ module StandardAPI
|
|
56
56
|
end
|
57
57
|
|
58
58
|
def filter_model_params(model_params, model, id: nil, allow_id: nil)
|
59
|
-
permitted_params = if self.respond_to?("#{model_name(model)}_attributes", true)
|
59
|
+
permitted_params = if model_params && self.respond_to?("#{model_name(model)}_attributes", true)
|
60
60
|
permits = self.send("#{model_name(model)}_attributes")
|
61
61
|
|
62
62
|
allow_id ? model_params.permit(permits, :id) : model_params.permit(permits)
|
@@ -67,7 +67,7 @@ module StandardAPI
|
|
67
67
|
if self.respond_to?("nested_#{model_name(model)}_attributes", true)
|
68
68
|
self.send("nested_#{model_name(model)}_attributes").each do |relation|
|
69
69
|
relation = model.reflect_on_association(relation)
|
70
|
-
attributes_key = "#{relation.name}
|
70
|
+
attributes_key = "#{relation.name}"
|
71
71
|
|
72
72
|
if model_params.has_key?(attributes_key)
|
73
73
|
filter_method = "filter_#{relation.klass.base_class.model_name.singular}_params"
|
@@ -77,15 +77,49 @@ module StandardAPI
|
|
77
77
|
permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params[attributes_key].map{|a| a['id']}
|
78
78
|
elsif self.respond_to?(filter_method, true)
|
79
79
|
permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
|
80
|
-
model_params[attributes_key].map { |i|
|
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
|
+
}
|
81
91
|
else
|
82
|
-
self.send(filter_method, model_params[attributes_key], allow_id: true)
|
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
|
83
100
|
end
|
84
101
|
else
|
85
102
|
permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
|
86
|
-
model_params[attributes_key].map { |i|
|
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
|
+
}
|
87
114
|
else
|
88
|
-
filter_model_params(model_params[attributes_key], relation.klass.base_class, allow_id: true)
|
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
|
89
123
|
end
|
90
124
|
end
|
91
125
|
elsif relation.collection? && model_params.has_key?("#{relation.name.to_s.singularize}_ids")
|
@@ -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,53 +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
117
|
association = resource.association(params[:relationship])
|
111
|
-
subresource = association.klass.find_by_id(params[:resource_id])
|
112
118
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
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)
|
118
125
|
end
|
119
|
-
head :no_content
|
120
|
-
else
|
121
|
-
head :not_found
|
122
126
|
end
|
127
|
+
head result ? :no_content : :not_found
|
123
128
|
end
|
124
129
|
|
125
130
|
def add_resource
|
126
131
|
resource = resources.find(params[:id])
|
127
132
|
association = resource.association(params[:relationship])
|
128
|
-
subresource = association.klass.
|
133
|
+
subresource = association.klass.find(params[:resource_id])
|
129
134
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
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)
|
137
158
|
else
|
138
|
-
|
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)
|
139
169
|
end
|
140
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
|
141
178
|
end
|
142
179
|
|
180
|
+
def mask
|
181
|
+
@mask ||= Hash.new do |hash, key|
|
182
|
+
hash[key] = mask_for(key)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
143
186
|
# Override if you want to support masking
|
144
|
-
def
|
145
|
-
|
187
|
+
def mask_for(table_name)
|
188
|
+
# case table_name
|
189
|
+
# when 'accounts'
|
190
|
+
# end
|
146
191
|
end
|
147
192
|
|
148
193
|
module ClassMethods
|
@@ -165,7 +210,11 @@ module StandardAPI
|
|
165
210
|
end
|
166
211
|
|
167
212
|
def model
|
168
|
-
|
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
|
169
218
|
end
|
170
219
|
|
171
220
|
def models
|
@@ -215,8 +264,7 @@ module StandardAPI
|
|
215
264
|
end
|
216
265
|
|
217
266
|
def resources
|
218
|
-
|
219
|
-
query = model.filter(params['where']).filter(mask)
|
267
|
+
query = self.class.model.filter(params['where']).filter(mask[self.class.model.table_name.to_sym])
|
220
268
|
|
221
269
|
if params[:distinct_on]
|
222
270
|
query = query.distinct_on(params[:distinct_on])
|
@@ -234,9 +282,29 @@ module StandardAPI
|
|
234
282
|
|
235
283
|
query
|
236
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
|
237
295
|
|
238
296
|
def includes
|
239
|
-
@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
|
240
308
|
end
|
241
309
|
|
242
310
|
def required_orders
|
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 = {}
|
@@ -97,13 +103,13 @@ module StandardAPI
|
|
97
103
|
|
98
104
|
case association = record.class.reflect_on_association(relation)
|
99
105
|
when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
|
100
|
-
"#{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)}"
|
101
107
|
when ActiveRecord::Reflection::BelongsToReflection
|
102
108
|
klass = association.options[:polymorphic] ? record.send(association.foreign_type).constantize : association.klass
|
103
109
|
if subincludes.empty?
|
104
|
-
"#{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)}"
|
105
111
|
else
|
106
|
-
"#{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)}"
|
107
113
|
end
|
108
114
|
else
|
109
115
|
raise ArgumentError, 'Unkown association type'
|
@@ -156,7 +162,9 @@ module StandardAPI
|
|
156
162
|
|
157
163
|
def json_column_type(sql_type)
|
158
164
|
case sql_type
|
159
|
-
when '
|
165
|
+
when 'binary', 'bytea'
|
166
|
+
'binary'
|
167
|
+
when /timestamp(\(\d+\))? without time zone/
|
160
168
|
'datetime'
|
161
169
|
when 'time without time zone'
|
162
170
|
'datetime'
|
@@ -164,9 +172,7 @@ module StandardAPI
|
|
164
172
|
'string'
|
165
173
|
when 'json'
|
166
174
|
'hash'
|
167
|
-
when 'bigint'
|
168
|
-
'integer'
|
169
|
-
when 'integer'
|
175
|
+
when 'smallint', 'bigint', 'integer'
|
170
176
|
'integer'
|
171
177
|
when 'jsonb'
|
172
178
|
'hash'
|
data/lib/standard_api/railtie.rb
CHANGED
@@ -20,4 +20,21 @@ module StandardAPI
|
|
20
20
|
end
|
21
21
|
|
22
22
|
end
|
23
|
+
|
24
|
+
module AutosaveByDefault
|
25
|
+
def self.included base
|
26
|
+
base.class_eval do
|
27
|
+
class <<self
|
28
|
+
alias_method :standard_build, :build
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.build(model, name, scope, options, &block)
|
32
|
+
options[:autosave] = true
|
33
|
+
standard_build(model, name, scope, options, &block)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
::ActiveRecord::Associations::Builder::Association.include(AutosaveByDefault)
|
23
40
|
end
|
@@ -22,11 +22,38 @@ module StandardAPI
|
|
22
22
|
options = resources.extract_options!.dup
|
23
23
|
|
24
24
|
resources(*resources, options) do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
block.call if block # custom routes take precedence over standardapi routes
|
26
|
+
|
27
|
+
available_actions = if only = parent_resource.instance_variable_get(:@only)
|
28
|
+
Array(only).map(&:to_sym)
|
29
|
+
else
|
30
|
+
if parent_resource.instance_variable_get(:@api_only)
|
31
|
+
[:index, :create, :show, :update, :destroy]
|
32
|
+
else
|
33
|
+
[:index, :create, :new, :show, :update, :destroy, :edit]
|
34
|
+
end + [ :schema, :calculate, :add_resource, :remove_resource, :create_resource ]
|
35
|
+
end
|
36
|
+
|
37
|
+
actions = if except = parent_resource.instance_variable_get(:@except)
|
38
|
+
available_actions - Array(except).map(&:to_sym)
|
39
|
+
else
|
40
|
+
available_actions
|
41
|
+
end
|
42
|
+
|
43
|
+
get :schema, on: :collection if actions.include?(:schema)
|
44
|
+
get :calculate, on: :collection if actions.include?(:calculate)
|
45
|
+
|
46
|
+
if actions.include?(:add_resource)
|
47
|
+
post ':relationship/:resource_id' => :add_resource, on: :member
|
48
|
+
end
|
49
|
+
|
50
|
+
if actions.include?(:create_resource)
|
51
|
+
post ':relationship' => :create_resource, on: :member
|
52
|
+
end
|
53
|
+
|
54
|
+
if actions.include?(:remove_resource)
|
55
|
+
delete ':relationship/:resource_id' => :remove_resource, on: :member
|
56
|
+
end
|
30
57
|
end
|
31
58
|
end
|
32
59
|
|
@@ -51,10 +78,33 @@ module StandardAPI
|
|
51
78
|
options = resource.extract_options!.dup
|
52
79
|
|
53
80
|
resource(*resource, options) do
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
81
|
+
available_actions = if only = parent_resource.instance_variable_get(:@only)
|
82
|
+
Array(only).map(&:to_sym)
|
83
|
+
else
|
84
|
+
if parent_resource.instance_variable_get(:@api_only)
|
85
|
+
[:index, :create, :show, :update, :destroy]
|
86
|
+
else
|
87
|
+
[:index, :create, :new, :show, :update, :destroy, :edit]
|
88
|
+
end + [ :schema, :calculate, :add_resource, :remove_resource ]
|
89
|
+
end
|
90
|
+
|
91
|
+
actions = if except = parent_resource.instance_variable_get(:@except)
|
92
|
+
available_actions - Array(except).map(&:to_sym)
|
93
|
+
else
|
94
|
+
available_actions
|
95
|
+
end
|
96
|
+
|
97
|
+
get :schema, on: :collection if actions.include?(:schema)
|
98
|
+
get :calculate, on: :collection if actions.include?(:calculate)
|
99
|
+
|
100
|
+
if actions.include?(:add_resource)
|
101
|
+
post ':relationship/:resource_id' => :add_resource, on: :member
|
102
|
+
end
|
103
|
+
|
104
|
+
if actions.include?(:remove_resource)
|
105
|
+
delete ':relationship/:resource_id' => :remove_resource, on: :member
|
106
|
+
end
|
107
|
+
|
58
108
|
block.call if block
|
59
109
|
end
|
60
110
|
end
|
@@ -59,22 +59,23 @@ module StandardAPI
|
|
59
59
|
# calculations
|
60
60
|
end
|
61
61
|
|
62
|
-
test '#calculate.json
|
62
|
+
test '#calculate.json mask_for' do
|
63
63
|
# This is just to instance @controller
|
64
64
|
get resource_path(:calculate)
|
65
65
|
|
66
|
-
# If #
|
67
|
-
# test other's implementation of #
|
68
|
-
return if @controller.method(:
|
66
|
+
# If #mask isn't defined by StandardAPI we don't know how to
|
67
|
+
# test other's implementation of #mask_for. Return and don't test.
|
68
|
+
return if @controller.method(:mask_for).owner != StandardAPI
|
69
69
|
|
70
70
|
m = create_model
|
71
71
|
|
72
|
-
@controller.
|
72
|
+
@controller.define_singleton_method(:mask_for) do |table_name|
|
73
|
+
{ id: m.id + 100 }
|
74
|
+
end
|
73
75
|
selects = [{ count: :id}, { maximum: :id }, { minimum: :id }, { average: :id }]
|
74
76
|
get :calculate, select: selects, format: 'json'
|
75
77
|
assert_response :ok
|
76
78
|
assert_equal [[0, nil, nil, nil]], @controller.instance_variable_get('@calculations')
|
77
|
-
@controller.current_mask.delete(plural_name)
|
78
79
|
end
|
79
80
|
|
80
81
|
end
|