standardapi 6.0.0.12 → 6.0.0.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/lib/standard_api.rb +1 -0
  3. data/lib/standard_api/controller.rb +44 -8
  4. data/lib/standard_api/errors.rb +13 -0
  5. data/lib/standard_api/helpers.rb +5 -5
  6. data/lib/standard_api/includes.rb +22 -12
  7. data/lib/standard_api/orders.rb +4 -6
  8. data/lib/standard_api/railtie.rb +1 -1
  9. data/lib/standard_api/route_helpers.rb +4 -0
  10. data/lib/standard_api/test_case/calculate_tests.rb +31 -14
  11. data/lib/standard_api/version.rb +1 -1
  12. data/lib/standard_api/views/application/_record.json.jbuilder +11 -10
  13. data/lib/standard_api/views/application/_record.streamer +11 -10
  14. data/lib/standard_api/views/application/index.json.jbuilder +9 -16
  15. data/lib/standard_api/views/application/index.streamer +9 -16
  16. data/lib/standard_api/views/application/show.json.jbuilder +8 -1
  17. data/lib/standard_api/views/application/show.streamer +8 -1
  18. data/test/standard_api/test_app.rb +54 -0
  19. data/test/standard_api/test_app/config/database.yml +4 -0
  20. data/test/standard_api/test_app/controllers.rb +107 -0
  21. data/test/standard_api/test_app/log/test.log +129516 -0
  22. data/test/standard_api/test_app/models.rb +89 -0
  23. data/test/standard_api/test_app/test/factories.rb +50 -0
  24. data/test/standard_api/test_app/test/fixtures/photo.png +0 -0
  25. data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +15 -0
  26. data/test/standard_api/test_app/views/photos/schema.json.jbuilder +1 -0
  27. data/test/standard_api/test_app/views/properties/edit.html.erb +1 -0
  28. data/test/standard_api/test_app/views/sessions/new.html.erb +0 -0
  29. metadata +21 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: dea6fe2070a9d9f2e92b545da5d2f7415568fee783ba45e1be6628822eb49fa7
4
- data.tar.gz: a4b14c21227e9790fe4913b173faadc310b7384d09c39cd1891b864fd98c2ff6
3
+ metadata.gz: 6327d5995d8b86de6c3f0513a922bc45115a002290fe0ad1e4f0cf639ade1427
4
+ data.tar.gz: 83bf723915282b111cd22f17845b6c30e9140e5bf03832966bae4c548c380388
5
5
  SHA512:
6
- metadata.gz: 451fbdbd66417b109b33d4bc1631f7a9e2913e345b966074baad332168c4184f5d8c7cda94f4e48afb771ef22bd4eb7d9b38986bbd1bd874ceef251001aeddc9
7
- data.tar.gz: cd109f69f5115e62a66719b1310af3d28a71ce7124d4cc873c139d44b59680edac0ff6330f935a9b1d2ddca2919d18a89fa7d7718d323689bffbed3ec3f04837
6
+ metadata.gz: c916b7ac39169c32ab99f285c4f3bc6cf8c67b790b4e8055c530c22658ed51382baf66e966b317a48996e8bc7820e0f483ac3bb0abc83ea0f803060cf220e1b6
7
+ data.tar.gz: f42efcea4be0ed86b9ff2ccd66799cadfb8058e3d5d32dfa02bffca257c644f011632da98d796c8fbf6295e6915e58bfdb4cab75941543a490da3a99976b0ab2
data/lib/standard_api.rb CHANGED
@@ -8,6 +8,7 @@ require 'active_record/sort'
8
8
  require 'active_support/core_ext/hash/indifferent_access'
9
9
 
10
10
  require 'standard_api/version'
11
+ require 'standard_api/errors'
11
12
  require 'standard_api/orders'
12
13
  require 'standard_api/includes'
13
14
  require 'standard_api/controller'
@@ -5,13 +5,14 @@ module StandardAPI
5
5
  klass.helper_method :includes, :orders, :model, :resource_limit,
6
6
  :default_limit, :preloadables
7
7
  klass.before_action :set_standardapi_headers
8
+ klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request
8
9
  klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
9
10
  klass.extend(ClassMethods)
10
11
  end
11
12
 
12
13
  def tables
13
- Rails.application.eager_load! if Rails.env == 'development'.freeze
14
-
14
+ Rails.application.eager_load! if !Rails.application.config.eager_load
15
+
15
16
  controllers = ApplicationController.descendants
16
17
  controllers.select! { |c| c.ancestors.include?(self.class) && c != self.class }
17
18
  controllers.map!(&:model).compact!
@@ -20,7 +21,8 @@ module StandardAPI
20
21
  end
21
22
 
22
23
  def index
23
- instance_variable_set("@#{model.model_name.plural}", resources.limit(limit).offset(params[:offset]).sort(orders))
24
+ records = preloadables(resources.limit(limit).offset(params[:offset]).sort(orders), includes)
25
+ instance_variable_set("@#{model.model_name.plural}", records)
24
26
  end
25
27
 
26
28
  def calculate
@@ -37,7 +39,8 @@ module StandardAPI
37
39
  end
38
40
 
39
41
  def show
40
- instance_variable_set("@#{model.model_name.singular}", resources.find(params[:id]))
42
+ record = preloadables(resources, includes).find(params[:id])
43
+ instance_variable_set("@#{model.model_name.singular}", record)
41
44
  end
42
45
 
43
46
  def new
@@ -92,6 +95,33 @@ module StandardAPI
92
95
  resources.find(params[:id]).destroy!
93
96
  head :no_content
94
97
  end
98
+
99
+ def remove_resource
100
+ resource = resources.find(params[:id])
101
+ subresource_class = resource.association(params[:relationship]).klass
102
+ subresource = subresource_class.find_by_id(params[:resource_id])
103
+
104
+ if(subresource)
105
+ result = resource.send(params[:relationship]).delete(subresource)
106
+ head result ? :no_content : :bad_request
107
+ else
108
+ head :not_found
109
+ end
110
+ end
111
+
112
+ def add_resource
113
+ resource = resources.find(params[:id])
114
+
115
+ subresource_class = resource.association(params[:relationship]).klass
116
+ subresource = subresource_class.find_by_id(params[:resource_id])
117
+ if(subresource)
118
+ result = resource.send(params[:relationship]) << subresource
119
+ head result ? :created : :bad_request
120
+ else
121
+ head :not_found
122
+ end
123
+
124
+ end
95
125
 
96
126
  # Override if you want to support masking
97
127
  def current_mask
@@ -109,6 +139,10 @@ module StandardAPI
109
139
 
110
140
  private
111
141
 
142
+ def bad_request(exception)
143
+ render body: exception.to_s, status: :bad_request
144
+ end
145
+
112
146
  def set_standardapi_headers
113
147
  headers['StandardAPI-Version'] = StandardAPI::VERSION
114
148
  end
@@ -175,13 +209,13 @@ module StandardAPI
175
209
  end
176
210
 
177
211
  def includes
178
- @includes ||= StandardAPI::Includes.normalize(params[:include])
212
+ @includes ||= StandardAPI::Includes.sanitize(params[:include], model_includes)
179
213
  end
180
214
 
181
- def preloadables(record, iclds)
215
+ def preloadables(record, includes)
182
216
  preloads = {}
183
217
 
184
- iclds.each do |key, value|
218
+ includes.each do |key, value|
185
219
  if reflection = record.klass.reflections[key]
186
220
  case value
187
221
  when true
@@ -307,7 +341,9 @@ module StandardAPI
307
341
 
308
342
  column = column == '*' ? Arel.star : column.to_sym
309
343
  if functions.include?(func.to_s.downcase)
310
- @selects << ((defined?(@model) ? @model : model).arel_table[column].send(func))
344
+ node = (defined?(@model) ? @model : model).arel_table[column].send(func)
345
+ node.distinct = true if params[:distinct]
346
+ @selects << node
311
347
  end
312
348
  end
313
349
  end
@@ -0,0 +1,13 @@
1
+ module StandardAPI
2
+ class StandardAPIError < StandardError
3
+ end
4
+
5
+ class UnpermittedParameters < StandardAPIError
6
+ attr_reader :params
7
+
8
+ def initialize(params)
9
+ @params = params
10
+ super("found unpermitted parameter#{'s' if params.size > 1 }: #{params.map { |e| e.inspect }.join(", ")}")
11
+ end
12
+ end
13
+ end
@@ -58,7 +58,7 @@ module StandardAPI
58
58
  end
59
59
 
60
60
  def cached_at_columns_for_includes(includes)
61
- includes.select { |k,v| !['when', 'where', 'limit', 'order', 'distinct'].include?(k) }.map do |k, v|
61
+ includes.select { |k,v| !['when', 'where', 'limit', 'order', 'distinct', 'distinct_on'].include?(k) }.map do |k, v|
62
62
  ["#{k}_cached_at"] + cached_at_columns_for_includes(v).map { |v2| "#{k}_#{v2}" }
63
63
  end.flatten
64
64
  end
@@ -103,8 +103,6 @@ module StandardAPI
103
103
 
104
104
  def json_column_type(sql_type)
105
105
  case sql_type
106
- when /character varying(\(\d+\))?/
107
- 'string'
108
106
  when 'timestamp without time zone'
109
107
  'datetime'
110
108
  when 'time without time zone'
@@ -133,10 +131,12 @@ module StandardAPI
133
131
  'string'
134
132
  when 'boolean'
135
133
  'boolean'
136
- when 'geometry'
137
- 'ewkb'
138
134
  when 'uuid' # TODO: should be uuid
139
135
  'string'
136
+ when /character varying(\(\d+\))?/
137
+ 'string'
138
+ when /^geometry/
139
+ 'ewkb'
140
140
  end
141
141
  end
142
142
 
@@ -17,17 +17,29 @@ module StandardAPI
17
17
  includes.flatten.compact.each { |v| normalized.merge!(normalize(v)) }
18
18
  when Hash, ActionController::Parameters
19
19
  includes.each_pair do |k, v|
20
- if ['limit', 'when', 'where', 'order'].include?(k.to_s) # Where and order are not normalized (sanitation happens in activerecord-filter)
21
- normalized[k] = case v
20
+ normalized[k] = case k.to_s
21
+ when 'when', 'where', 'order'
22
+ case v
22
23
  when Hash then v.to_h
23
24
  when ActionController::Parameters then v.to_unsafe_h
24
25
  end
25
- elsif k.to_s == 'distinct'
26
- normalized[k] = case v
26
+ when 'limit'
27
+ case v
28
+ when String then v.to_i
29
+ when Integer then v
30
+ end
31
+ when 'distinct'
32
+ case v
33
+ when 'true' then true
34
+ when 'false' then false
35
+ end
36
+ when 'distinct_on'
37
+ case v
27
38
  when String then v
39
+ when Array then v
28
40
  end
29
41
  else
30
- normalized[k] = normalize(v)
42
+ normalize(v)
31
43
  end
32
44
  end
33
45
  when nil
@@ -58,14 +70,12 @@ module StandardAPI
58
70
 
59
71
  permit = normalize(permit.with_indifferent_access)
60
72
  includes.each do |k, v|
61
- if permit.has_key?(k) || ['limit', 'when', 'where', 'order', 'distinct'].include?(k.to_s)
62
- permitted[k] = sanitize(v, permit[k] || {}, true)
73
+ permitted[k] = if permit.has_key?(k)
74
+ sanitize(v, permit[k] || {}, true)
75
+ elsif ['limit', 'when', 'where', 'order', 'distinct', 'distinct_on'].include?(k.to_s)
76
+ v
63
77
  else
64
- if [:raise, nil].include?(Rails.configuration.try(:action_on_unpermitted_includes))
65
- raise ActionController::UnpermittedParameters.new([k])
66
- else
67
- Rails.logger.try(:warn, "Invalid Include: #{k}")
68
- end
78
+ raise StandardAPI::UnpermittedParameters.new([k])
69
79
  end
70
80
  end
71
81
 
@@ -15,11 +15,11 @@ module StandardAPI
15
15
  key2, key3 = *key.to_s.split('.')
16
16
  permitted << sanitize({key2.to_sym => { key3.to_sym => value } }, permit)
17
17
  elsif permit.include?(key.to_s)
18
- case value
18
+ value = case value
19
19
  when Hash
20
20
  value
21
21
  when ActionController::Parameters
22
- value.to_unsafe_hash
22
+ value.permit([:asc, :desc]).to_h
23
23
  else
24
24
  value
25
25
  end
@@ -29,7 +29,7 @@ module StandardAPI
29
29
  sanitized_value = sanitize(value, subpermit)
30
30
  permitted << { key.to_sym => sanitized_value }
31
31
  else
32
- raise(ActionController::UnpermittedParameters.new([orders]))
32
+ raise(StandardAPI::UnpermittedParameters.new([orders]))
33
33
  end
34
34
  end
35
35
  when Array
@@ -48,7 +48,7 @@ module StandardAPI
48
48
  elsif permit.include?(orders.to_s)
49
49
  permitted = orders
50
50
  else
51
- raise(ActionController::UnpermittedParameters.new([orders]))
51
+ raise(StandardAPI::UnpermittedParameters.new([orders]))
52
52
  end
53
53
  end
54
54
 
@@ -57,8 +57,6 @@ module StandardAPI
57
57
  else
58
58
  permitted
59
59
  end
60
-
61
- # permitted
62
60
  end
63
61
 
64
62
  end
@@ -9,4 +9,4 @@ module StandardAPI
9
9
  end
10
10
 
11
11
  end
12
- end
12
+ end
@@ -24,6 +24,8 @@ module StandardAPI
24
24
  resources(*resources, options) do
25
25
  get :schema, on: :collection
26
26
  get :calculate, on: :collection
27
+ delete ':relationship/:resource_id' => :remove_resource, on: :member
28
+ post ':relationship/:resource_id' => :add_resource, on: :member
27
29
  block.call if block
28
30
  end
29
31
  end
@@ -51,6 +53,8 @@ module StandardAPI
51
53
  resource(*resource, options) do
52
54
  get :schema, on: :collection
53
55
  get :calculate, on: :collection
56
+ delete ':relationship/:resource_id' => :remove_resource, on: :member
57
+ post ':relationship/:resource_id' => :add_resource, on: :member
54
58
  block.call if block
55
59
  end
56
60
  end
@@ -3,37 +3,54 @@ module StandardAPI
3
3
  module CalculateTests
4
4
  extend ActiveSupport::Testing::Declarative
5
5
 
6
- CALCULATE_COLUMN_TYPES = ["smallint", "int", "bigint", "real", "double precision", "numeric", "interval"]
6
+ CALCULATE_COLUMN_TYPES = [
7
+ "smallint", "int", "integer", "bigint", "real", "double precision",
8
+ "numeric", "interval"
9
+ ]
7
10
 
8
11
  test '#calculate.json' do
9
12
  create_model
10
13
 
11
- column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }.name
12
- selects = [{ count: column}, { maximum: column }, { minimum: column }, { average: column }]
14
+ math_column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }
15
+
16
+ if math_column
17
+ column = math_column
18
+ selects = [{ count: column.name }, { maximum: column.name }, { minimum: column.name }, { average: column.name }]
19
+ else
20
+ column = model.columns.sample
21
+ selects = [{ count: column.name }]
22
+ end
13
23
 
14
24
  get resource_path(:calculate, select: selects, format: :json)
15
25
  assert_response :ok
16
26
  calculations = @controller.instance_variable_get('@calculations')
17
- assert_equal [[model.count(column), model.maximum(column), model.minimum(column), model.average(column).to_f]], calculations
27
+ expectations = selects.map { |s| model.send(s.keys.first, column.name) }
28
+ expectations = [expectations] if expectations.length > 1
29
+ assert_equal expectations,
30
+ calculations
18
31
  end
19
32
 
20
33
  test '#calculate.json params[:where]' do
21
34
  m1 = create_model
22
35
  create_model
23
36
 
24
- column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }.name
25
- selects = [{ count: column}, { maximum: column }, { minimum: column }, { average: column }]
37
+ math_column = model.columns.find { |x| CALCULATE_COLUMN_TYPES.include?(x.sql_type) }
38
+
39
+ if math_column
40
+ column = math_column
41
+ selects = [{ count: column.name}, { maximum: column.name }, { minimum: column.name }, { average: column.name }]
42
+ else
43
+ column = model.columns.sample
44
+ selects = [{ count: column.name}]
45
+ end
46
+
26
47
  predicate = { id: { gt: m1.id } }
27
48
 
28
49
  get resource_path(:calculate, where: predicate, select: selects, format: :json)
29
-
30
- # assert_response :ok
31
- # assert_equal [[
32
- # model.filter(predicate).count(column),
33
- # model.filter(predicate).maximum(column),
34
- # model.filter(predicate).minimum(column),
35
- # model.filter(predicate).average(column).to_f
36
- # ]], @controller.instance_variable_get('@calculations')
50
+ assert_response :ok
51
+ calculations = @controller.instance_variable_get('@calculations')
52
+ # assert_equal [selects.map { |s| model.send(s.keys.first, column.name) }],
53
+ # calculations
37
54
  end
38
55
 
39
56
  test '#calculate.json mask' do
@@ -1,3 +1,3 @@
1
1
  module StandardAPI
2
- VERSION = '6.0.0.12'
2
+ VERSION = '6.0.0.26'
3
3
  end
@@ -5,7 +5,7 @@ record.attributes.each do |name, value|
5
5
  end
6
6
 
7
7
  includes.each do |inc, subinc|
8
- next if ["limit", "offset", "order", "when", "where"].include?(inc)
8
+ next if ["limit", "offset", "order", "when", "where", "distinct", "distinct_on"].include?(inc)
9
9
 
10
10
  case association = record.class.reflect_on_association(inc)
11
11
  when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::ThroughReflection
@@ -14,15 +14,16 @@ includes.each do |inc, subinc|
14
14
  partial = model_partial(association.klass)
15
15
  json.set! inc do
16
16
  # TODO limit causes preloaded assocations to reload
17
- if subinc.keys.any? { |x| ["limit", "offset", "order", "when", "where"].include?(x) }
18
- if subinc['distinct']
19
- json.array! record.send(inc).filter(subinc['where']).limit(subinc['limit']).sort(subinc['order']).distinct_on(subinc['distinct']), partial: partial, as: partial.split('/').last, locals: { includes: subinc }
20
- else
21
- json.array! record.send(inc).filter(subinc['where']).limit(subinc['limit']).sort(subinc['order']).distinct, partial: partial, as: partial.split('/').last, locals: { includes: subinc }
22
- end
23
- else
24
- json.array! record.send(inc), partial: partial, as: partial.split('/').last, locals: { includes: subinc }
25
- end
17
+ sub_records = record.send(inc)
18
+
19
+ sub_records = sub_records.limit(subinc['limit']) if subinc['limit']
20
+ sub_records = sub_records.offset(subinc['offset']) if subinc['offset']
21
+ sub_records = sub_records.order(subinc['order']) if subinc['order']
22
+ sub_records = sub_records.filter(subinc['where']) if subinc['where']
23
+ sub_records = sub_records.distinct if subinc['distinct']
24
+ sub_records = sub_records.distinct_on(subinc['distinct_on']) if subinc['distinct_on']
25
+
26
+ json.array! sub_records, partial: partial, as: partial.split('/').last, locals: { includes: subinc }
26
27
  end
27
28
  end
28
29
  when ActiveRecord::Reflection::BelongsToReflection, ActiveRecord::Reflection::HasOneReflection
@@ -7,7 +7,7 @@ json.object! do
7
7
  end
8
8
 
9
9
  includes.each do |inc, subinc|
10
- next if ["limit", "offset", "order", "when", "where"].include?(inc)
10
+ next if ["limit", "offset", "order", "when", "where", "distinct", "distinct_on"].include?(inc)
11
11
 
12
12
  case association = record.class.reflect_on_association(inc)
13
13
  when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::ThroughReflection
@@ -16,15 +16,16 @@ json.object! do
16
16
  partial = model_partial(association.klass)
17
17
  json.set! inc do
18
18
  # TODO limit causes preloaded assocations to reload
19
- if subinc.keys.any? { |x| ["limit", "offset", "order", "when", "where"].include?(x) }
20
- if subinc['distinct']
21
- json.array! record.send(inc).filter(subinc['where']).limit(subinc['limit']).sort(subinc['order']).distinct_on(subinc['distinct']), partial: partial, as: partial.split('/').last, locals: { includes: subinc }
22
- else
23
- json.array! record.send(inc).filter(subinc['where']).limit(subinc['limit']).sort(subinc['order']).distinct, partial: partial, as: partial.split('/').last, locals: { includes: subinc }
24
- end
25
- else
26
- json.array! record.send(inc), partial: partial, as: partial.split('/').last, locals: { includes: subinc }
27
- end
19
+ sub_records = record.send(inc)
20
+
21
+ sub_records = sub_records.limit(subinc['limit']) if subinc['limit']
22
+ sub_records = sub_records.offset(subinc['offset']) if subinc['offset']
23
+ sub_records = sub_records.order(subinc['order']) if subinc['order']
24
+ sub_records = sub_records.filter(subinc['where']) if subinc['where']
25
+ sub_records = sub_records.distinct if subinc['distinct']
26
+ sub_records = sub_records.distinct_on(subinc['distinct_on']) if subinc['distinct_on']
27
+
28
+ json.array! sub_records, partial: partial, as: partial.split('/').last, locals: { includes: subinc }
28
29
  end
29
30
  end
30
31
  when ActiveRecord::Reflection::BelongsToReflection, ActiveRecord::Reflection::HasOneReflection
@@ -1,19 +1,16 @@
1
- if !includes.empty?
2
- instance_variable_set("@#{model.model_name.plural}", preloadables(instance_variable_get("@#{model.model_name.plural}"), includes))
1
+ if !defined?(records)
2
+ records = instance_variable_get("@#{model.model_name.plural}")
3
3
  end
4
4
 
5
- if !includes.empty? && can_cache?(model, includes)
6
- partial = model_partial(model)
7
- record_name = partial.split('/').last.to_sym
5
+ partial = model_partial(model)
6
+ partial_record_name = partial.split('/').last.to_sym
8
7
 
9
- json.cache_collection! instance_variable_get("@#{model.model_name.plural}"), key: proc { |record| cache_key(record, includes) } do |record|
10
- locals = { record: record, record_name => record, :includes => includes }
11
- json.partial! partial, locals
8
+ if !includes.empty? && can_cache?(model, includes)
9
+ json.cache_collection! records, key: proc { |record| cache_key(record, includes) } do |record|
10
+ json.partial!(partial, includes: includes, partial_record_name => record)
12
11
  end
13
12
  else
14
- partial = model_partial(model)
15
- record_name = partial.split('/').last.to_sym
16
- json.array!(instance_variable_get("@#{model.model_name.plural}")) do |record|
13
+ json.array!(records) do |record|
17
14
  sub_includes = includes.select do |key, value|
18
15
  case value
19
16
  when Hash, ActionController::Parameters
@@ -27,10 +24,6 @@ else
27
24
  end
28
25
  end
29
26
 
30
- json.partial! partial, {
31
- record: record,
32
- record_name => record,
33
- includes: sub_includes
34
- }
27
+ json.partial!(partial, includes: sub_includes, partial_record_name => record)
35
28
  end
36
29
  end