standardapi 6.0.0.15 → 6.0.0.29

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -6
  3. data/lib/standard_api.rb +2 -0
  4. data/lib/standard_api/active_record/connection_adapters/postgresql/schema_statements.rb +21 -0
  5. data/lib/standard_api/controller.rb +77 -66
  6. data/lib/standard_api/errors.rb +13 -0
  7. data/lib/standard_api/helpers.rb +67 -15
  8. data/lib/standard_api/includes.rb +22 -12
  9. data/lib/standard_api/orders.rb +4 -6
  10. data/lib/standard_api/railtie.rb +1 -1
  11. data/lib/standard_api/route_helpers.rb +4 -0
  12. data/lib/standard_api/test_case/calculate_tests.rb +5 -3
  13. data/lib/standard_api/test_case/create_tests.rb +0 -3
  14. data/lib/standard_api/test_case/schema_tests.rb +19 -3
  15. data/lib/standard_api/version.rb +1 -1
  16. data/lib/standard_api/views/application/_record.json.jbuilder +11 -10
  17. data/lib/standard_api/views/application/_record.streamer +11 -10
  18. data/lib/standard_api/views/application/_schema.json.jbuilder +68 -0
  19. data/lib/standard_api/views/application/_schema.streamer +78 -0
  20. data/lib/standard_api/views/application/index.json.jbuilder +9 -16
  21. data/lib/standard_api/views/application/index.streamer +9 -16
  22. data/lib/standard_api/views/application/schema.json.jbuilder +1 -12
  23. data/lib/standard_api/views/application/schema.streamer +1 -16
  24. data/lib/standard_api/views/application/show.json.jbuilder +8 -1
  25. data/lib/standard_api/views/application/show.streamer +8 -1
  26. data/test/standard_api/test_app.rb +55 -0
  27. data/test/standard_api/test_app/config/database.yml +4 -0
  28. data/test/standard_api/test_app/controllers.rb +107 -0
  29. data/test/standard_api/test_app/models.rb +94 -0
  30. data/test/standard_api/test_app/test/factories.rb +50 -0
  31. data/test/standard_api/test_app/test/fixtures/photo.png +0 -0
  32. data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +15 -0
  33. data/test/standard_api/test_app/views/photos/_photo.streamer +17 -0
  34. data/test/standard_api/test_app/views/photos/_schema.json.jbuilder +1 -0
  35. data/test/standard_api/test_app/views/photos/_schema.streamer +3 -0
  36. data/test/standard_api/test_app/views/photos/schema.json.jbuilder +1 -0
  37. data/test/standard_api/test_app/views/photos/schema.streamer +1 -0
  38. data/test/standard_api/test_app/views/properties/edit.html.erb +1 -0
  39. data/test/standard_api/test_app/views/sessions/new.html.erb +0 -0
  40. metadata +27 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bbc6b3f7345e1df82060abcc37ee0bcf5f11b88863e1cdab4299f514129b44f6
4
- data.tar.gz: cae2c3d6859933c99ac13f0704e35997e3c10603f53d325b3a8ace63607fce94
3
+ metadata.gz: 3670132e90418137f095c8e7741310e2911bfde21e78579e27ea6c54ae88d0f1
4
+ data.tar.gz: 205778759e7188eda509e289495a383d6a5ff037116fd5043a519f95c5b7222d
5
5
  SHA512:
6
- metadata.gz: 75a1555b14bbbcfd99f2dcd31cbba472a157c0066a22d597cd1fff9daa7c4f53e1c6461c21c8adde888715a90df07ae7e4e8eb48467b702a7ba45e6dba21de10
7
- data.tar.gz: a9deedd375a788e22502b4caea542fdbe935ac6f4d594d8e46ceb79bcf9202e5813b795df9c60c7360ef84a9068b5388d862e95734e9bd05f427f4286ff3d114
6
+ metadata.gz: 2d4a009d0ea47b18ebc5088d81bdf4518e9a22749b1ea0a17e8f7b5504be42dbeea60945ad75eeb5a4cdfda866f39edc12391262ff64b2a5cc3748011cb0c4bc
7
+ data.tar.gz: 49a638815647e3fbe6d63dc4eda87eda4703425b2c36e4565763cc0aad1abf0934d54d8f18858d00ac733f1e4662873f5ff586e1cdf27aafdaebfbc825703b71
data/README.md CHANGED
@@ -105,11 +105,12 @@ including the author, the photos that the author took can also be included.
105
105
  # API Usage
106
106
  Resources can be queried via REST style end points
107
107
  ```
108
- GET /records/:id fetch record
109
- PATCH /records/:id update record
110
- GET /records fetch records
111
- POST /records create record
112
- DELETE /records destroy record
108
+ GET /records/:id fetch record
109
+ PATCH /records/:id update record
110
+ GET /records/ fetch records
111
+ GET /records/calculate apply count and other functions on record(s)
112
+ POST /records create record
113
+ DELETE /records destroy record
113
114
  ```
114
115
 
115
116
  All resource end points can be filtered, ordered, limited, offset, and have includes. All options are passed via query string in a nested URI encoded format.
@@ -163,8 +164,16 @@ location: {within: 0106000020e6...} WHERE ST_Within("listings"."location
163
164
 
164
165
  // On Relationships
165
166
  property: {size: 10000} JOIN properties WHERE properties.size = 10000"
167
+ ```
168
+ ## Calculations
169
+
170
+ The only change on calculate routes is the `selects` paramater contains the functions to apply. Currently just `minimum`, `maximum`, `average`, `sum`, and `count`.
166
171
 
167
- //
172
+ ```
173
+ { count: '*' } SELECT COUNT(*)
174
+ [{ count: '*' }] SELECT COUNT(*)
175
+ [{ count: '*', maximum: :id, minimum: :id }] SELECT COUNT(*), MAXIMUM(id), MINIMUM(id)
176
+ [{ maximum: :id }, { maximum: :count }] SELECT MAXIMUM(id), MAXIMUM(count)
168
177
  ```
169
178
 
170
179
  # Testing
data/lib/standard_api.rb CHANGED
@@ -8,9 +8,11 @@ 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'
14
15
  require 'standard_api/helpers'
15
16
  require 'standard_api/route_helpers'
17
+ require 'standard_api/active_record/connection_adapters/postgresql/schema_statements'
16
18
  require 'standard_api/railtie'
@@ -0,0 +1,21 @@
1
+ module ActiveRecord
2
+ module ConnectionAdapters
3
+ class PostgreSQLAdapter < AbstractAdapter
4
+
5
+ # Returns a comment stored in database for given table
6
+ def database_comment(database_name=nil) # :nodoc:
7
+ database_name ||= current_database
8
+
9
+ scope = quoted_scope(database_name, type: "BASE TABLE")
10
+ if scope[:name]
11
+ query_value(<<~SQL, "SCHEMA")
12
+ SELECT pg_catalog.shobj_description(d.oid, 'pg_database')
13
+ FROM pg_catalog.pg_database d
14
+ WHERE datname = #{scope[:name]};
15
+ SQL
16
+ end
17
+ end
18
+
19
+ end
20
+ end
21
+ end
@@ -1,26 +1,36 @@
1
1
  module StandardAPI
2
2
  module Controller
3
3
 
4
+ delegate :preloadables, to: :helpers
5
+
4
6
  def self.included(klass)
5
- klass.helper_method :includes, :orders, :model, :resource_limit,
6
- :default_limit, :preloadables
7
+ klass.helper_method :includes, :orders, :model, :models, :resource_limit,
8
+ :default_limit
7
9
  klass.before_action :set_standardapi_headers
10
+ klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request
8
11
  klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
9
12
  klass.extend(ClassMethods)
10
13
  end
11
14
 
12
15
  def tables
13
- Rails.application.eager_load! if Rails.env == 'development'.freeze
14
-
15
- controllers = ApplicationController.descendants
16
- controllers.select! { |c| c.ancestors.include?(self.class) && c != self.class }
17
- controllers.map!(&:model).compact!
18
- controllers.map!(&:table_name)
19
- render json: controllers
16
+ Rails.application.eager_load! if !Rails.application.config.eager_load
17
+
18
+ tables = ApplicationController.descendants
19
+ tables.select! { |c| c.ancestors.include?(self.class) && c != self.class }
20
+ tables.map!(&:model).compact!
21
+ tables.map!(&:table_name)
22
+ render json: tables
23
+ end
24
+
25
+ if Rails.env == 'development'
26
+ def schema
27
+ Rails.application.eager_load! if !Rails.application.config.eager_load
28
+ end
20
29
  end
21
30
 
22
31
  def index
23
- instance_variable_set("@#{model.model_name.plural}", resources.limit(limit).offset(params[:offset]).sort(orders))
32
+ records = preloadables(resources.limit(limit).offset(params[:offset]).sort(orders), includes)
33
+ instance_variable_set("@#{model.model_name.plural}", records)
24
34
  end
25
35
 
26
36
  def calculate
@@ -32,12 +42,13 @@ module StandardAPI
32
42
  end
33
43
  end
34
44
  @calculations = Hash[@calculations] if @calculations[0].is_a?(Array) && params[:group_by]
35
-
45
+
36
46
  render json: @calculations
37
47
  end
38
48
 
39
49
  def show
40
- instance_variable_set("@#{model.model_name.singular}", resources.find(params[:id]))
50
+ record = preloadables(resources, includes).find(params[:id])
51
+ instance_variable_set("@#{model.model_name.singular}", record)
41
52
  end
42
53
 
43
54
  def new
@@ -93,13 +104,40 @@ module StandardAPI
93
104
  head :no_content
94
105
  end
95
106
 
107
+ def remove_resource
108
+ resource = resources.find(params[:id])
109
+ subresource_class = resource.association(params[:relationship]).klass
110
+ subresource = subresource_class.find_by_id(params[:resource_id])
111
+
112
+ if(subresource)
113
+ result = resource.send(params[:relationship]).delete(subresource)
114
+ head result ? :no_content : :bad_request
115
+ else
116
+ head :not_found
117
+ end
118
+ end
119
+
120
+ def add_resource
121
+ resource = resources.find(params[:id])
122
+
123
+ subresource_class = resource.association(params[:relationship]).klass
124
+ subresource = subresource_class.find_by_id(params[:resource_id])
125
+ if(subresource)
126
+ result = resource.send(params[:relationship]) << subresource
127
+ head result ? :created : :bad_request
128
+ else
129
+ head :not_found
130
+ end
131
+
132
+ end
133
+
96
134
  # Override if you want to support masking
97
135
  def current_mask
98
136
  @current_mask ||= {}
99
137
  end
100
138
 
101
139
  module ClassMethods
102
-
140
+
103
141
  def model
104
142
  return @model if defined?(@model)
105
143
  @model = name.sub(/Controller\z/, '').singularize.camelize.safe_constantize
@@ -109,6 +147,10 @@ module StandardAPI
109
147
 
110
148
  private
111
149
 
150
+ def bad_request(exception)
151
+ render body: exception.to_s, status: :bad_request
152
+ end
153
+
112
154
  def set_standardapi_headers
113
155
  headers['StandardAPI-Version'] = StandardAPI::VERSION
114
156
  end
@@ -117,6 +159,15 @@ module StandardAPI
117
159
  self.class.model
118
160
  end
119
161
 
162
+ def models
163
+ return @models if defined?(@models)
164
+ Rails.application.eager_load! if !Rails.application.config.eager_load
165
+
166
+ @models = ApplicationController.descendants
167
+ @models.select! { |c| c.ancestors.include?(self.class) && c != self.class }
168
+ @models.map!(&:model).compact!
169
+ end
170
+
120
171
  def model_includes
121
172
  if self.respond_to?("#{model.model_name.singular}_includes", true)
122
173
  self.send("#{model.model_name.singular}_includes")
@@ -156,81 +207,39 @@ module StandardAPI
156
207
 
157
208
  def resources
158
209
  query = model.filter(params['where']).filter(current_mask[model.table_name])
159
-
210
+
160
211
  if params[:distinct_on]
161
212
  query = query.distinct_on(params[:distinct_on])
162
213
  elsif params[:distinct]
163
214
  query = query.distinct
164
215
  end
165
-
216
+
166
217
  if params[:join]
167
218
  query = query.joins(params[:join].to_sym)
168
219
  end
169
-
220
+
170
221
  if params[:group_by]
171
222
  query = query.group(params[:group_by])
172
223
  end
173
-
224
+
174
225
  query
175
226
  end
176
227
 
177
228
  def includes
178
- @includes ||= StandardAPI::Includes.normalize(params[:include])
179
- end
180
-
181
- def preloadables(record, iclds)
182
- preloads = {}
183
-
184
- iclds.each do |key, value|
185
- if reflection = record.klass.reflections[key]
186
- case value
187
- when true
188
- preloads[key] = value
189
- when Hash, ActiveSupport::HashWithIndifferentAccess
190
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
191
- if !reflection.polymorphic?
192
- preloads[key] = preloadables_hash(reflection.klass, value)
193
- end
194
- end
195
- end
196
- end
197
- end
198
-
199
- preloads.empty? ? record : record.preload(preloads)
200
- end
201
-
202
- def preloadables_hash(klass, iclds)
203
- preloads = {}
204
-
205
- iclds.each do |key, value|
206
- if reflection = klass.reflections[key]
207
- case value
208
- when true
209
- preloads[key] = value
210
- when Hash, ActiveSupport::HashWithIndifferentAccess
211
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
212
- if !reflection.polymorphic?
213
- preloads[key] = preloadables_hash(reflection.klass, value)
214
- end
215
- end
216
- end
217
- end
218
- end
219
-
220
- preloads
229
+ @includes ||= StandardAPI::Includes.sanitize(params[:include], model_includes)
221
230
  end
222
-
231
+
223
232
  def required_orders
224
233
  []
225
234
  end
226
-
235
+
227
236
  def default_orders
228
237
  nil
229
238
  end
230
239
 
231
240
  def orders
232
241
  exluded_required_orders = required_orders.map(&:to_s)
233
-
242
+
234
243
  case params[:order]
235
244
  when Hash, ActionController::Parameters
236
245
  exluded_required_orders -= params[:order].keys.map(&:to_s)
@@ -246,7 +255,7 @@ module StandardAPI
246
255
  when String
247
256
  exluded_required_orders.delete(params[:order])
248
257
  end
249
-
258
+
250
259
  if !exluded_required_orders.empty?
251
260
  params[:order] = exluded_required_orders.unshift(params[:order])
252
261
  end
@@ -304,10 +313,12 @@ module StandardAPI
304
313
  @model = parts[0].singularize.camelize.constantize
305
314
  column = parts[1]
306
315
  end
307
-
316
+
308
317
  column = column == '*' ? Arel.star : column.to_sym
309
318
  if functions.include?(func.to_s.downcase)
310
- @selects << ((defined?(@model) ? @model : model).arel_table[column].send(func))
319
+ node = (defined?(@model) ? @model : model).arel_table[column].send(func)
320
+ node.distinct = true if params[:distinct]
321
+ @selects << node
311
322
  end
312
323
  end
313
324
  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
@@ -1,6 +1,58 @@
1
1
  module StandardAPI
2
2
  module Helpers
3
-
3
+
4
+ def preloadables(record, includes)
5
+ preloads = {}
6
+
7
+ includes.each do |key, value|
8
+ if reflection = record.klass.reflections[key]
9
+ case value
10
+ when true
11
+ preloads[key] = value
12
+ when Hash, ActiveSupport::HashWithIndifferentAccess
13
+ 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
17
+ end
18
+ end
19
+ end
20
+ end
21
+
22
+ preloads.empty? ? record : record.preload(preloads)
23
+ end
24
+
25
+ def preloadables_hash(klass, iclds)
26
+ preloads = {}
27
+
28
+ 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
39
+ end
40
+ end
41
+ end
42
+
43
+ preloads
44
+ end
45
+
46
+ def schema_partial(model)
47
+ path = model.model_name.plural
48
+
49
+ if lookup_context.exists?("schema", path, true)
50
+ [path, "schema"].join('/')
51
+ else
52
+ 'application/schema'
53
+ end
54
+ end
55
+
4
56
  def model_partial(record)
5
57
  if lookup_context.exists?(record.model_name.element, record.model_name.plural, true)
6
58
  [record.model_name.plural, record.model_name.element].join('/')
@@ -17,7 +69,7 @@ module StandardAPI
17
69
  false
18
70
  end
19
71
  end
20
-
72
+
21
73
  def cache_key(record, includes)
22
74
  timestamp_keys = ['cached_at'] + record.class.column_names.select{|x| x.ends_with? "_cached_at"}
23
75
  if includes.empty?
@@ -27,7 +79,7 @@ module StandardAPI
27
79
  "#{record.model_name.cache_key}/#{record.id}-#{digest_hash(sort_hash(includes))}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
28
80
  end
29
81
  end
30
-
82
+
31
83
  def can_cache_relation?(klass, relation, subincludes)
32
84
  cache_columns = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
33
85
  if (cache_columns - klass.column_names).empty?
@@ -36,12 +88,12 @@ module StandardAPI
36
88
  false
37
89
  end
38
90
  end
39
-
91
+
40
92
  def association_cache_key(record, relation, subincludes)
41
93
  timestamp = ["#{relation}_cached_at"] + cached_at_columns_for_includes(subincludes).map {|c| "#{relation}_#{c}"}
42
94
  timestamp.map! { |col| record.send(col) }
43
95
  timestamp = timestamp.max
44
-
96
+
45
97
  case association = record.class.reflect_on_association(relation)
46
98
  when ActiveRecord::Reflection::HasManyReflection, ActiveRecord::Reflection::HasAndBelongsToManyReflection, ActiveRecord::Reflection::HasOneReflection, ActiveRecord::Reflection::ThroughReflection
47
99
  "#{record.model_name.cache_key}/#{record.id}/#{includes_to_cache_key(relation, subincludes)}-#{timestamp.utc.to_s(record.cache_timestamp_format)}"
@@ -56,13 +108,13 @@ module StandardAPI
56
108
  raise ArgumentError, 'Unkown association type'
57
109
  end
58
110
  end
59
-
111
+
60
112
  def cached_at_columns_for_includes(includes)
61
- includes.select { |k,v| !['when', 'where', 'limit', 'order', 'distinct'].include?(k) }.map do |k, v|
113
+ includes.select { |k,v| !['when', 'where', 'limit', 'order', 'distinct', 'distinct_on'].include?(k) }.map do |k, v|
62
114
  ["#{k}_cached_at"] + cached_at_columns_for_includes(v).map { |v2| "#{k}_#{v2}" }
63
115
  end.flatten
64
116
  end
65
-
117
+
66
118
  def includes_to_cache_key(relation, subincludes)
67
119
  if subincludes.empty?
68
120
  relation.to_s
@@ -70,7 +122,7 @@ module StandardAPI
70
122
  "#{relation}-#{digest_hash(sort_hash(subincludes))}"
71
123
  end
72
124
  end
73
-
125
+
74
126
  def sort_hash(hash)
75
127
  hash.keys.sort.reduce({}) do |seed, key|
76
128
  if seed[key].is_a?(Hash)
@@ -81,7 +133,7 @@ module StandardAPI
81
133
  seed
82
134
  end
83
135
  end
84
-
136
+
85
137
  def digest_hash(*hashes)
86
138
  hashes.compact!
87
139
  hashes.map! { |h| sort_hash(h) }
@@ -103,8 +155,6 @@ module StandardAPI
103
155
 
104
156
  def json_column_type(sql_type)
105
157
  case sql_type
106
- when /character varying(\(\d+\))?/
107
- 'string'
108
158
  when 'timestamp without time zone'
109
159
  'datetime'
110
160
  when 'time without time zone'
@@ -133,12 +183,14 @@ module StandardAPI
133
183
  'string'
134
184
  when 'boolean'
135
185
  'boolean'
136
- when 'geometry'
137
- 'ewkb'
138
186
  when 'uuid' # TODO: should be uuid
139
187
  'string'
188
+ when /character varying(\(\d+\))?/
189
+ 'string'
190
+ when /^geometry/
191
+ 'ewkb'
140
192
  end
141
193
  end
142
194
 
143
195
  end
144
- end
196
+ end