standardapi 6.1.0 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +80 -58
  3. data/lib/standard_api/access_control_list.rb +40 -6
  4. data/lib/standard_api/controller.rb +96 -28
  5. data/lib/standard_api/helpers.rb +22 -22
  6. data/lib/standard_api/middleware.rb +5 -0
  7. data/lib/standard_api/railtie.rb +17 -0
  8. data/lib/standard_api/route_helpers.rb +59 -9
  9. data/lib/standard_api/test_case/calculate_tests.rb +7 -6
  10. data/lib/standard_api/test_case/destroy_tests.rb +19 -7
  11. data/lib/standard_api/test_case/index_tests.rb +7 -13
  12. data/lib/standard_api/test_case/show_tests.rb +7 -7
  13. data/lib/standard_api/test_case/update_tests.rb +7 -6
  14. data/lib/standard_api/version.rb +1 -1
  15. data/lib/standard_api/views/application/_record.json.jbuilder +17 -6
  16. data/lib/standard_api/views/application/_record.streamer +4 -3
  17. data/lib/standard_api/views/application/_schema.json.jbuilder +20 -8
  18. data/lib/standard_api/views/application/_schema.streamer +22 -8
  19. data/lib/standard_api.rb +1 -0
  20. data/test/standard_api/caching_test.rb +2 -2
  21. data/test/standard_api/controller/include_test.rb +107 -0
  22. data/test/standard_api/controller/subresource_test.rb +157 -0
  23. data/test/standard_api/helpers_test.rb +9 -8
  24. data/test/standard_api/nested_attributes/belongs_to_test.rb +71 -0
  25. data/test/standard_api/nested_attributes/has_and_belongs_to_many_test.rb +70 -0
  26. data/test/standard_api/nested_attributes/has_many_test.rb +85 -0
  27. data/test/standard_api/nested_attributes/has_one_test.rb +71 -0
  28. data/test/standard_api/route_helpers_test.rb +56 -0
  29. data/test/standard_api/standard_api_test.rb +110 -50
  30. data/test/standard_api/test_app/app/controllers/acl/account_acl.rb +5 -1
  31. data/test/standard_api/test_app/app/controllers/acl/camera_acl.rb +7 -0
  32. data/test/standard_api/test_app/app/controllers/acl/photo_acl.rb +13 -0
  33. data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +7 -1
  34. data/test/standard_api/test_app/controllers.rb +17 -0
  35. data/test/standard_api/test_app/models.rb +59 -2
  36. data/test/standard_api/test_app/test/factories.rb +3 -0
  37. data/test/standard_api/test_app/views/sessions/create.json.jbuilder +1 -0
  38. data/test/standard_api/test_app/views/sessions/create.streamer +3 -0
  39. data/test/standard_api/test_app.rb +13 -1
  40. data/test/standard_api/test_helper.rb +100 -7
  41. metadata +52 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7134ec77418da381ff4cb9aa8717f797784ab1f4d45faefde89261e6e7a82ad9
4
- data.tar.gz: 83e924e7fd2ded8c5d4239f9d775ef232fa985a4daa838aec374105eed65df2e
3
+ metadata.gz: cf0f6e2d86659b79df8b56eb7b998118541facdc0fd516ad878f4afb0c531299
4
+ data.tar.gz: dcfebcfd01bd96dbaa8a4a2aef53ea1549c0820b7879845a2e812311efbd3556
5
5
  SHA512:
6
- metadata.gz: 6e477baa763d068d112a29a9539145b75edf47be726c21e679fc4842ae63157914c8b2753f0f7e338c43fe2e328cbe85c4cb3681958113b5533af478bcb43fc3
7
- data.tar.gz: 551a57b70b62f0f6c27f97aba97930b220a40b80799058a7c1a35d0d4a0afa787dd760ac01d42110d050a4d0d0f1387d6b00d63629c523c96e0b49a6910b77c1
6
+ metadata.gz: e7b0189b209ce50457ba90a8f2c89653557f1f7ff61b0a955f479c40beefc8666f47e9dfe9c2e6b17193fc197510305ff442ea49c0bf31ab482011d729b7b09e
7
+ data.tar.gz: a31458fac536e7d0e074a3c4c5efee56123f264771316503df1ff69b8a4539e782e29c109d5c9f89a789d5f54b576300fde5de91852dc4a67a526b665bc793ff
data/README.md CHANGED
@@ -1,36 +1,27 @@
1
1
  # StandardAPI
2
2
 
3
- StandardAPI makes it easy to expost a [REST](https://en.wikipedia.org/wiki/Representational_state_transfer)
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
- In `config/application.rb:
14
+ Optionally in `config/application.rb`:
15
15
 
16
- require_relative 'boot'
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 5.2
19
+ config.load_defaults 7.0
29
20
 
30
- # Settings in config/environments/* take precedence over those specified here.
31
- # Application configuration can go into files in config/initializers
32
- # -- all .rb files in that directory are automatically loaded after loading
33
- # the framework and any gems in your application.
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
- If greater control of the allowed paramaters is required, the `model_params`
63
- method can be overridden. It simply returns a set of `StrongParameters`.
66
+ ##### Access Control List
64
67
 
65
- class PhotosController < ApplicationController
66
- include StandardAPI
67
-
68
- def model_params
69
- if @photo.author == current_user
70
- [:caption]
71
- else
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
- Similarly, the ordering and includes (including of relationships in the reponse)
78
- is whitelisted as well.
77
+ Then create an ACL file for each model you want in `app/controllers/acl`.
79
78
 
80
- Full Example:
79
+ Taking the above example we would remove the `photo_*` methods and create the
80
+ following files:
81
81
 
82
- class PhotosController < ApplicationController
83
- including StandardAPI
82
+ `app/controllers/acl/photo_acl.rb`:
84
83
 
85
- # Allowed paramaters
86
- def photo_params
87
- [:caption]
84
+ module PhotoACL
85
+ # Allowed attributes
86
+ def attributes
87
+ [ :caption ]
88
88
  end
89
-
90
- # Allowed orderings
91
- def photo_orders
92
- [:id, :created_at, :updated_at]
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 photo_includes
97
- { author: [:photos] }
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
- Note how includes can also support nested includes. So in this case when
103
- including the author, the photos that the author took can also be included.
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 current_mask
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}_attributes"
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| self.send(filter_method, i, allow_id: true) }
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| filter_model_params(i, relation.klass.base_class, allow_id: true) }
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 :edit, status: :bad_request
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
- render :show, status: :bad_request
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]).destroy!
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
- if(subresource)
114
- if association.is_a? ActiveRecord::Associations::HasManyAssociation
115
- resource.send(params[:relationship]).delete(subresource)
116
- else
117
- resource.send("#{params[:relationship]}=", nil)
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.find_by_id(params[:resource_id])
133
+ subresource = association.klass.find(params[:resource_id])
129
134
 
130
- if(subresource)
131
- if association.is_a? ActiveRecord::Associations::HasManyAssociation
132
- result = resource.send(params[:relationship]) << subresource
133
- else
134
- result = resource.send("#{params[:relationship]}=", subresource)
135
- end
136
- head result ? :created : :bad_request
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
- head :not_found
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 current_mask
145
- @current_mask ||= {}
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
- self.class.model
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
- mask = current_mask[model.table_name] || current_mask[model.table_name.to_sym]
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 ||= StandardAPI::Includes.sanitize(params[:include], model_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
@@ -1,6 +1,12 @@
1
1
  module StandardAPI
2
2
  module Helpers
3
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
9
+
4
10
  def preloadables(record, includes)
5
11
  preloads = {}
6
12
 
@@ -11,31 +17,25 @@ module StandardAPI
11
17
  preloads[key] = value
12
18
  when Hash, ActiveSupport::HashWithIndifferentAccess
13
19
  if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
14
- if !reflection.polymorphic?
15
- preloads[key] = preloadables_hash(reflection.klass, value)
16
- end
20
+ preloads[key.to_sym] = preloadables_hash(value)
17
21
  end
18
22
  end
19
23
  end
20
24
  end
21
25
 
22
- preloads.empty? ? record : record.preload(preloads)
26
+ preloads.present? ? record.preload(preloads) : record
23
27
  end
24
28
 
25
- def preloadables_hash(klass, iclds)
29
+ def preloadables_hash(iclds)
26
30
  preloads = {}
27
31
 
28
32
  iclds.each do |key, value|
29
- if reflection = klass.reflections[key]
30
- case value
31
- when true
32
- preloads[key] = value
33
- when Hash, ActiveSupport::HashWithIndifferentAccess
34
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
35
- if !reflection.polymorphic?
36
- preloads[key] = preloadables_hash(reflection.klass, value)
37
- end
38
- end
33
+ case value
34
+ when true
35
+ preloads[key] = value
36
+ when Hash, ActiveSupport::HashWithIndifferentAccess
37
+ if !value.keys.any? { |x| [ 'when', 'where', 'limit', 'offset', 'order', 'distinct' ].include?(x) }
38
+ preloads[key] = preloadables_hash(value)
39
39
  end
40
40
  end
41
41
  end
@@ -97,13 +97,13 @@ module StandardAPI
97
97
 
98
98
  case association = record.class.reflect_on_association(relation)
99
99
  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.to_s(record.cache_timestamp_format)}"
100
+ "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_fs(record.cache_timestamp_format)}"
101
101
  when ActiveRecord::Reflection::BelongsToReflection
102
102
  klass = association.options[:polymorphic] ? record.send(association.foreign_type).constantize : association.klass
103
103
  if subincludes.empty?
104
- "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.to_s(klass.cache_timestamp_format)}"
104
+ "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}-#{timestamp.utc.to_fs(klass.cache_timestamp_format)}"
105
105
  else
106
- "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.to_s(klass.cache_timestamp_format)}"
106
+ "#{klass.model_name.cache_key}/#{record.send(association.foreign_key)}/#{digest_hash(sort_hash(subincludes))}-#{timestamp.utc.to_fs(klass.cache_timestamp_format)}"
107
107
  end
108
108
  else
109
109
  raise ArgumentError, 'Unkown association type'
@@ -156,7 +156,9 @@ module StandardAPI
156
156
 
157
157
  def json_column_type(sql_type)
158
158
  case sql_type
159
- when 'timestamp without time zone'
159
+ when 'binary', 'bytea'
160
+ 'binary'
161
+ when /timestamp(\(\d+\))? without time zone/
160
162
  'datetime'
161
163
  when 'time without time zone'
162
164
  'datetime'
@@ -164,9 +166,7 @@ module StandardAPI
164
166
  'string'
165
167
  when 'json'
166
168
  'hash'
167
- when 'bigint'
168
- 'integer'
169
- when 'integer'
169
+ when 'smallint', 'bigint', 'integer'
170
170
  'integer'
171
171
  when 'jsonb'
172
172
  'hash'
@@ -0,0 +1,5 @@
1
+ module StandardAPI
2
+ module Middleware
3
+ autoload :QueryEncoding, 'standard_api/middleware/query_encoding'
4
+ end
5
+ end
@@ -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