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.
- checksums.yaml +4 -4
- data/lib/standard_api.rb +1 -0
- data/lib/standard_api/controller.rb +44 -8
- data/lib/standard_api/errors.rb +13 -0
- data/lib/standard_api/helpers.rb +5 -5
- data/lib/standard_api/includes.rb +22 -12
- data/lib/standard_api/orders.rb +4 -6
- data/lib/standard_api/railtie.rb +1 -1
- data/lib/standard_api/route_helpers.rb +4 -0
- data/lib/standard_api/test_case/calculate_tests.rb +31 -14
- data/lib/standard_api/version.rb +1 -1
- data/lib/standard_api/views/application/_record.json.jbuilder +11 -10
- data/lib/standard_api/views/application/_record.streamer +11 -10
- data/lib/standard_api/views/application/index.json.jbuilder +9 -16
- data/lib/standard_api/views/application/index.streamer +9 -16
- data/lib/standard_api/views/application/show.json.jbuilder +8 -1
- data/lib/standard_api/views/application/show.streamer +8 -1
- data/test/standard_api/test_app.rb +54 -0
- data/test/standard_api/test_app/config/database.yml +4 -0
- data/test/standard_api/test_app/controllers.rb +107 -0
- data/test/standard_api/test_app/log/test.log +129516 -0
- data/test/standard_api/test_app/models.rb +89 -0
- data/test/standard_api/test_app/test/factories.rb +50 -0
- data/test/standard_api/test_app/test/fixtures/photo.png +0 -0
- data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +15 -0
- data/test/standard_api/test_app/views/photos/schema.json.jbuilder +1 -0
- data/test/standard_api/test_app/views/properties/edit.html.erb +1 -0
- data/test/standard_api/test_app/views/sessions/new.html.erb +0 -0
- metadata +21 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6327d5995d8b86de6c3f0513a922bc45115a002290fe0ad1e4f0cf639ade1427
|
4
|
+
data.tar.gz: 83bf723915282b111cd22f17845b6c30e9140e5bf03832966bae4c548c380388
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c916b7ac39169c32ab99f285c4f3bc6cf8c67b790b4e8055c530c22658ed51382baf66e966b317a48996e8bc7820e0f483ac3bb0abc83ea0f803060cf220e1b6
|
7
|
+
data.tar.gz: f42efcea4be0ed86b9ff2ccd66799cadfb8058e3d5d32dfa02bffca257c644f011632da98d796c8fbf6295e6915e58bfdb4cab75941543a490da3a99976b0ab2
|
data/lib/standard_api.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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.
|
212
|
+
@includes ||= StandardAPI::Includes.sanitize(params[:include], model_includes)
|
179
213
|
end
|
180
214
|
|
181
|
-
def preloadables(record,
|
215
|
+
def preloadables(record, includes)
|
182
216
|
preloads = {}
|
183
217
|
|
184
|
-
|
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
|
-
|
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
|
data/lib/standard_api/helpers.rb
CHANGED
@@ -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
|
-
|
21
|
-
|
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
|
-
|
26
|
-
|
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
|
-
|
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)
|
62
|
-
|
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
|
-
|
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
|
|
data/lib/standard_api/orders.rb
CHANGED
@@ -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.
|
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(
|
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(
|
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
|
data/lib/standard_api/railtie.rb
CHANGED
@@ -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 = [
|
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
|
-
|
12
|
-
|
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
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
31
|
-
# assert_equal [
|
32
|
-
#
|
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
|
data/lib/standard_api/version.rb
CHANGED
@@ -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
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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 !
|
2
|
-
|
1
|
+
if !defined?(records)
|
2
|
+
records = instance_variable_get("@#{model.model_name.plural}")
|
3
3
|
end
|
4
4
|
|
5
|
-
|
6
|
-
|
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
|
-
|
10
|
-
|
11
|
-
json.partial!
|
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
|
-
|
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!
|
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
|