standardapi 6.0.0.15 → 6.0.0.29

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