standardapi 6.0.0.26 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +15 -6
  3. data/lib/standard_api.rb +5 -0
  4. data/lib/standard_api/access_control_list.rb +114 -0
  5. data/lib/standard_api/active_record/connection_adapters/postgresql/schema_statements.rb +21 -0
  6. data/lib/standard_api/controller.rb +75 -78
  7. data/lib/standard_api/errors.rb +9 -0
  8. data/lib/standard_api/helpers.rb +66 -13
  9. data/lib/standard_api/includes.rb +9 -0
  10. data/lib/standard_api/middleware/query_encoding.rb +3 -3
  11. data/lib/standard_api/railtie.rb +13 -2
  12. data/lib/standard_api/route_helpers.rb +5 -5
  13. data/lib/standard_api/test_case.rb +24 -14
  14. data/lib/standard_api/test_case/calculate_tests.rb +10 -4
  15. data/lib/standard_api/test_case/create_tests.rb +13 -15
  16. data/lib/standard_api/test_case/index_tests.rb +14 -4
  17. data/lib/standard_api/test_case/schema_tests.rb +25 -3
  18. data/lib/standard_api/test_case/show_tests.rb +1 -0
  19. data/lib/standard_api/test_case/update_tests.rb +8 -9
  20. data/lib/standard_api/version.rb +1 -1
  21. data/lib/standard_api/views/application/_record.json.jbuilder +33 -30
  22. data/lib/standard_api/views/application/_record.streamer +36 -34
  23. data/lib/standard_api/views/application/_schema.json.jbuilder +68 -0
  24. data/lib/standard_api/views/application/_schema.streamer +78 -0
  25. data/lib/standard_api/views/application/new.streamer +1 -1
  26. data/lib/standard_api/views/application/schema.json.jbuilder +1 -12
  27. data/lib/standard_api/views/application/schema.streamer +1 -16
  28. data/test/standard_api/caching_test.rb +43 -0
  29. data/test/standard_api/helpers_test.rb +172 -0
  30. data/test/standard_api/performance.rb +39 -0
  31. data/test/standard_api/route_helpers_test.rb +33 -0
  32. data/test/standard_api/standard_api_test.rb +699 -0
  33. data/test/standard_api/test_app.rb +1 -0
  34. data/test/standard_api/test_app/app/controllers/acl/account_acl.rb +15 -0
  35. data/test/standard_api/test_app/app/controllers/acl/property_acl.rb +27 -0
  36. data/test/standard_api/test_app/app/controllers/acl/reference_acl.rb +7 -0
  37. data/test/standard_api/test_app/controllers.rb +13 -45
  38. data/test/standard_api/test_app/models.rb +38 -4
  39. data/test/standard_api/test_app/test/factories.rb +4 -3
  40. data/test/standard_api/test_app/views/photos/_photo.json.jbuilder +1 -0
  41. data/test/standard_api/test_app/views/photos/_photo.streamer +18 -0
  42. data/test/standard_api/test_app/views/photos/_schema.json.jbuilder +1 -0
  43. data/test/standard_api/test_app/views/photos/_schema.streamer +3 -0
  44. data/test/standard_api/test_app/views/photos/schema.json.jbuilder +1 -1
  45. data/test/standard_api/test_app/views/photos/schema.streamer +1 -0
  46. data/test/standard_api/test_helper.rb +238 -0
  47. metadata +33 -17
  48. data/test/standard_api/test_app/log/test.log +0 -129516
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6327d5995d8b86de6c3f0513a922bc45115a002290fe0ad1e4f0cf639ade1427
4
- data.tar.gz: 83bf723915282b111cd22f17845b6c30e9140e5bf03832966bae4c548c380388
3
+ metadata.gz: 7134ec77418da381ff4cb9aa8717f797784ab1f4d45faefde89261e6e7a82ad9
4
+ data.tar.gz: 83e924e7fd2ded8c5d4239f9d775ef232fa985a4daa838aec374105eed65df2e
5
5
  SHA512:
6
- metadata.gz: c916b7ac39169c32ab99f285c4f3bc6cf8c67b790b4e8055c530c22658ed51382baf66e966b317a48996e8bc7820e0f483ac3bb0abc83ea0f803060cf220e1b6
7
- data.tar.gz: f42efcea4be0ed86b9ff2ccd66799cadfb8058e3d5d32dfa02bffca257c644f011632da98d796c8fbf6295e6915e58bfdb4cab75941543a490da3a99976b0ab2
6
+ metadata.gz: 6e477baa763d068d112a29a9539145b75edf47be726c21e679fc4842ae63157914c8b2753f0f7e338c43fe2e328cbe85c4cb3681958113b5533af478bcb43fc3
7
+ data.tar.gz: 551a57b70b62f0f6c27f97aba97930b220a40b80799058a7c1a35d0d4a0afa787dd760ac01d42110d050a4d0d0f1387d6b00d63629c523c96e0b49a6910b77c1
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
@@ -14,4 +14,9 @@ require 'standard_api/includes'
14
14
  require 'standard_api/controller'
15
15
  require 'standard_api/helpers'
16
16
  require 'standard_api/route_helpers'
17
+ require 'standard_api/active_record/connection_adapters/postgresql/schema_statements'
17
18
  require 'standard_api/railtie'
19
+
20
+ module StandardAPI
21
+ autoload :AccessControlList, 'standard_api/access_control_list'
22
+ end
@@ -0,0 +1,114 @@
1
+ module StandardAPI
2
+ module AccessControlList
3
+
4
+ def self.traverse(path, prefix: nil, &block)
5
+ path.children.each do |child|
6
+ if child.file? && child.basename('.rb').to_s.ends_with?('_acl')
7
+ block.call([prefix, child.basename('.rb').to_s].compact.join('/'))
8
+ elsif child.directory?
9
+ traverse(child, prefix: [prefix, child.basename.to_s].compact.join('/'), &block)
10
+ end
11
+ end
12
+ end
13
+
14
+ def self.included(application_controller)
15
+ acl_dir = Rails.application.root.join('app', 'controllers', 'acl')
16
+ return if !acl_dir.exist?
17
+
18
+ traverse(acl_dir) do |child|
19
+ mod = child.classify.constantize
20
+ prefix = child.delete_suffix('_acl').gsub('/', '_')
21
+
22
+ [:orders, :includes, :attributes].each do |m|
23
+ next if !mod.instance_methods.include?(m)
24
+ mod.send :alias_method, "#{prefix}_#{m}".to_sym, m
25
+ mod.send :remove_method, m
26
+ end
27
+
28
+ if mod.instance_methods.include?(:nested)
29
+ mod.send :alias_method, "nested_#{prefix}_attributes".to_sym, :nested
30
+ mod.send :remove_method, :nested
31
+ end
32
+
33
+ if mod.instance_methods.include?(:filter)
34
+ mod.send :alias_method, "filter_#{prefix}_params".to_sym, :filter
35
+ mod.send :remove_method, :filter
36
+ end
37
+
38
+ application_controller.include mod
39
+ end
40
+ end
41
+
42
+ def model_orders
43
+ if self.respond_to?("#{model.model_name.singular}_orders", true)
44
+ self.send("#{model.model_name.singular}_orders")
45
+ else
46
+ []
47
+ end
48
+ end
49
+
50
+ def model_params
51
+ if self.respond_to?("filter_#{model_name(model)}_params", true)
52
+ self.send("filter_#{model_name(model)}_params", params[model_name(model)], id: params[:id])
53
+ else
54
+ filter_model_params(params[model_name(model)], model.base_class)
55
+ end
56
+ end
57
+
58
+ def filter_model_params(model_params, model, id: nil, allow_id: nil)
59
+ permitted_params = if self.respond_to?("#{model_name(model)}_attributes", true)
60
+ permits = self.send("#{model_name(model)}_attributes")
61
+
62
+ allow_id ? model_params.permit(permits, :id) : model_params.permit(permits)
63
+ else
64
+ ActionController::Parameters.new
65
+ end
66
+
67
+ if self.respond_to?("nested_#{model_name(model)}_attributes", true)
68
+ self.send("nested_#{model_name(model)}_attributes").each do |relation|
69
+ relation = model.reflect_on_association(relation)
70
+ attributes_key = "#{relation.name}_attributes"
71
+
72
+ if model_params.has_key?(attributes_key)
73
+ filter_method = "filter_#{relation.klass.base_class.model_name.singular}_params"
74
+ if model_params[attributes_key].nil?
75
+ permitted_params[attributes_key] = nil
76
+ elsif model_params[attributes_key].is_a?(Array) && model_params[attributes_key].all? { |a| a.keys.map(&:to_sym) == [:id] }
77
+ permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params[attributes_key].map{|a| a['id']}
78
+ elsif self.respond_to?(filter_method, true)
79
+ permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
80
+ model_params[attributes_key].map { |i| self.send(filter_method, i, allow_id: true) }
81
+ else
82
+ self.send(filter_method, model_params[attributes_key], allow_id: true)
83
+ end
84
+ else
85
+ permitted_params[attributes_key] = if model_params[attributes_key].is_a?(Array)
86
+ model_params[attributes_key].map { |i| filter_model_params(i, relation.klass.base_class, allow_id: true) }
87
+ else
88
+ filter_model_params(model_params[attributes_key], relation.klass.base_class, allow_id: true)
89
+ end
90
+ end
91
+ elsif relation.collection? && model_params.has_key?("#{relation.name.to_s.singularize}_ids")
92
+ permitted_params["#{relation.name.to_s.singularize}_ids"] = model_params["#{relation.name.to_s.singularize}_ids"]
93
+ elsif model_params.has_key?(relation.foreign_key)
94
+ permitted_params[relation.foreign_key] = model_params[relation.foreign_key]
95
+ permitted_params[relation.foreign_type] = model_params[relation.foreign_type] if relation.polymorphic?
96
+ end
97
+
98
+ permitted_params.permit!
99
+ end
100
+ end
101
+
102
+ permitted_params
103
+ end
104
+
105
+ def model_name(model)
106
+ if model.model_name.singular.starts_with?('habtm_')
107
+ model.reflect_on_all_associations.map { |a| a.klass.base_class.model_name.singular }.sort.join('_')
108
+ else
109
+ model.model_name.singular
110
+ end
111
+ end
112
+
113
+ end
114
+ end
@@ -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,10 +1,13 @@
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::ParameterMissing, with: :bad_request
8
11
  klass.rescue_from StandardAPI::UnpermittedParameters, with: :bad_request
9
12
  klass.append_view_path(File.join(File.dirname(__FILE__), 'views'))
10
13
  klass.extend(ClassMethods)
@@ -13,11 +16,17 @@ module StandardAPI
13
16
  def tables
14
17
  Rails.application.eager_load! if !Rails.application.config.eager_load
15
18
 
16
- controllers = ApplicationController.descendants
17
- controllers.select! { |c| c.ancestors.include?(self.class) && c != self.class }
18
- controllers.map!(&:model).compact!
19
- controllers.map!(&:table_name)
20
- render json: controllers
19
+ tables = ApplicationController.descendants
20
+ tables.select! { |c| c.ancestors.include?(self.class) && c != self.class }
21
+ tables.map!(&:model).compact!
22
+ tables.map!(&:table_name)
23
+ render json: tables
24
+ end
25
+
26
+ if Rails.env == 'development'
27
+ def schema
28
+ Rails.application.eager_load! if !Rails.application.config.eager_load
29
+ end
21
30
  end
22
31
 
23
32
  def index
@@ -34,7 +43,7 @@ module StandardAPI
34
43
  end
35
44
  end
36
45
  @calculations = Hash[@calculations] if @calculations[0].is_a?(Array) && params[:group_by]
37
-
46
+
38
47
  render json: @calculations
39
48
  end
40
49
 
@@ -95,32 +104,40 @@ module StandardAPI
95
104
  resources.find(params[:id]).destroy!
96
105
  head :no_content
97
106
  end
98
-
107
+
99
108
  def remove_resource
100
109
  resource = resources.find(params[:id])
101
- subresource_class = resource.association(params[:relationship]).klass
102
- subresource = subresource_class.find_by_id(params[:resource_id])
103
-
110
+ association = resource.association(params[:relationship])
111
+ subresource = association.klass.find_by_id(params[:resource_id])
112
+
104
113
  if(subresource)
105
- result = resource.send(params[:relationship]).delete(subresource)
106
- head result ? :no_content : :bad_request
114
+ if association.is_a? ActiveRecord::Associations::HasManyAssociation
115
+ resource.send(params[:relationship]).delete(subresource)
116
+ else
117
+ resource.send("#{params[:relationship]}=", nil)
118
+ end
119
+ head :no_content
107
120
  else
108
121
  head :not_found
109
122
  end
110
123
  end
111
-
124
+
112
125
  def add_resource
113
126
  resource = resources.find(params[:id])
114
-
115
- subresource_class = resource.association(params[:relationship]).klass
116
- subresource = subresource_class.find_by_id(params[:resource_id])
127
+ association = resource.association(params[:relationship])
128
+ subresource = association.klass.find_by_id(params[:resource_id])
129
+
117
130
  if(subresource)
118
- result = resource.send(params[:relationship]) << subresource
131
+ if association.is_a? ActiveRecord::Associations::HasManyAssociation
132
+ result = resource.send(params[:relationship]) << subresource
133
+ else
134
+ result = resource.send("#{params[:relationship]}=", subresource)
135
+ end
119
136
  head result ? :created : :bad_request
120
137
  else
121
138
  head :not_found
122
139
  end
123
-
140
+
124
141
  end
125
142
 
126
143
  # Override if you want to support masking
@@ -129,7 +146,7 @@ module StandardAPI
129
146
  end
130
147
 
131
148
  module ClassMethods
132
-
149
+
133
150
  def model
134
151
  return @model if defined?(@model)
135
152
  @model = name.sub(/Controller\z/, '').singularize.camelize.safe_constantize
@@ -151,6 +168,15 @@ module StandardAPI
151
168
  self.class.model
152
169
  end
153
170
 
171
+ def models
172
+ return @models if defined?(@models)
173
+ Rails.application.eager_load! if !Rails.application.config.eager_load
174
+
175
+ @models = ApplicationController.descendants
176
+ @models.select! { |c| c.ancestors.include?(self.class) && c != self.class }
177
+ @models.map!(&:model).compact!
178
+ end
179
+
154
180
  def model_includes
155
181
  if self.respond_to?("#{model.model_name.singular}_includes", true)
156
182
  self.send("#{model.model_name.singular}_includes")
@@ -171,7 +197,7 @@ module StandardAPI
171
197
  if self.respond_to?("#{model.model_name.singular}_params", true)
172
198
  params.require(model.model_name.singular).permit(self.send("#{model.model_name.singular}_params"))
173
199
  else
174
- []
200
+ ActionController::Parameters.new
175
201
  end
176
202
  end
177
203
 
@@ -189,82 +215,41 @@ module StandardAPI
189
215
  end
190
216
 
191
217
  def resources
192
- query = model.filter(params['where']).filter(current_mask[model.table_name])
193
-
218
+ mask = current_mask[model.table_name] || current_mask[model.table_name.to_sym]
219
+ query = model.filter(params['where']).filter(mask)
220
+
194
221
  if params[:distinct_on]
195
222
  query = query.distinct_on(params[:distinct_on])
196
223
  elsif params[:distinct]
197
224
  query = query.distinct
198
225
  end
199
-
226
+
200
227
  if params[:join]
201
228
  query = query.joins(params[:join].to_sym)
202
229
  end
203
-
230
+
204
231
  if params[:group_by]
205
232
  query = query.group(params[:group_by])
206
233
  end
207
-
234
+
208
235
  query
209
236
  end
210
237
 
211
238
  def includes
212
239
  @includes ||= StandardAPI::Includes.sanitize(params[:include], model_includes)
213
240
  end
214
-
215
- def preloadables(record, includes)
216
- preloads = {}
217
-
218
- includes.each do |key, value|
219
- if reflection = record.klass.reflections[key]
220
- case value
221
- when true
222
- preloads[key] = value
223
- when Hash, ActiveSupport::HashWithIndifferentAccess
224
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
225
- if !reflection.polymorphic?
226
- preloads[key] = preloadables_hash(reflection.klass, value)
227
- end
228
- end
229
- end
230
- end
231
- end
232
-
233
- preloads.empty? ? record : record.preload(preloads)
234
- end
235
-
236
- def preloadables_hash(klass, iclds)
237
- preloads = {}
238
-
239
- iclds.each do |key, value|
240
- if reflection = klass.reflections[key]
241
- case value
242
- when true
243
- preloads[key] = value
244
- when Hash, ActiveSupport::HashWithIndifferentAccess
245
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
246
- if !reflection.polymorphic?
247
- preloads[key] = preloadables_hash(reflection.klass, value)
248
- end
249
- end
250
- end
251
- end
252
- end
253
-
254
- preloads
255
- end
256
-
241
+
257
242
  def required_orders
258
243
  []
259
244
  end
260
-
245
+
261
246
  def default_orders
262
247
  nil
263
248
  end
264
249
 
265
250
  def orders
266
251
  exluded_required_orders = required_orders.map(&:to_s)
267
-
252
+
268
253
  case params[:order]
269
254
  when Hash, ActionController::Parameters
270
255
  exluded_required_orders -= params[:order].keys.map(&:to_s)
@@ -280,7 +265,7 @@ module StandardAPI
280
265
  when String
281
266
  exluded_required_orders.delete(params[:order])
282
267
  end
283
-
268
+
284
269
  if !exluded_required_orders.empty?
285
270
  params[:order] = exluded_required_orders.unshift(params[:order])
286
271
  end
@@ -309,9 +294,9 @@ module StandardAPI
309
294
  limit = params.permit(:limit)[:limit]&.to_i || default_limit
310
295
 
311
296
  if !limit
312
- raise ActionController::ParameterMissing.new(:limit)
297
+ raise StandardAPI::ParameterMissing.new(:limit)
313
298
  elsif limit > resource_limit
314
- raise ActionController::UnpermittedParameters.new([:limit, limit])
299
+ raise StandardAPI::UnpermittedParameters.new([:limit, limit])
315
300
  end
316
301
 
317
302
  limit
@@ -333,16 +318,28 @@ module StandardAPI
333
318
  @selects = []
334
319
  @selects << params[:group_by] if params[:group_by]
335
320
  Array(params[:select]).each do |select|
336
- select.each do |func, column|
321
+ select.each do |func, value|
322
+ distinct = false
323
+
324
+ column = case value
325
+ when ActionController::Parameters
326
+ # TODO: Add support for other aggregate expressions
327
+ # https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES
328
+ distinct = !value[:distinct].nil?
329
+ value[:distinct]
330
+ else
331
+ value
332
+ end
333
+
337
334
  if (parts = column.split(".")).length > 1
338
335
  @model = parts[0].singularize.camelize.constantize
339
336
  column = parts[1]
340
337
  end
341
-
338
+
342
339
  column = column == '*' ? Arel.star : column.to_sym
343
340
  if functions.include?(func.to_s.downcase)
344
341
  node = (defined?(@model) ? @model : model).arel_table[column].send(func)
345
- node.distinct = true if params[:distinct]
342
+ node.distinct = distinct
346
343
  @selects << node
347
344
  end
348
345
  end