standardapi 6.0.0.12 → 6.0.0.26

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