standardapi 6.0.0.26 → 6.1.0

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