standardapi 6.1.0 → 7.1.1

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.
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