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