standardapi 7.1.0 → 7.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 88dd56c94d20d8649c5f755509be1aee880f7dff0133602a4fe46c0408c4e65b
4
- data.tar.gz: 3f14301f672ac171f9e2a39620de9f1913300572164978857ea12fcb1bb008de
3
+ metadata.gz: cf0f6e2d86659b79df8b56eb7b998118541facdc0fd516ad878f4afb0c531299
4
+ data.tar.gz: dcfebcfd01bd96dbaa8a4a2aef53ea1549c0820b7879845a2e812311efbd3556
5
5
  SHA512:
6
- metadata.gz: e3c282a6b8898d1c1451e381dea4cf57c3cd58b65bccfba6a0b5f9d302242287bb233dd43b4a078c51f1711a32a449d47c157626a92cc752939d8040ef704534
7
- data.tar.gz: 56d90b5dc3d5d04924b7bd5e5fe6a98cfe9cf4f6fa79d30c933abff3e5c6f6b69a6718a699bb5199183f05372f99d8038f99dc7e5ff1b011dca4da978ee4d5ae
6
+ metadata.gz: e7b0189b209ce50457ba90a8f2c89653557f1f7ff61b0a955f479c40beefc8666f47e9dfe9c2e6b17193fc197510305ff442ea49c0bf31ab482011d729b7b09e
7
+ data.tar.gz: a31458fac536e7d0e074a3c4c5efee56123f264771316503df1ff69b8a4539e782e29c109d5c9f89a789d5f54b576300fde5de91852dc4a67a526b665bc793ff
@@ -1,9 +1,9 @@
1
1
  module StandardAPI
2
2
  module Helpers
3
-
3
+
4
4
  def serialize_attribute(json, record, name, type)
5
5
  value = record.send(name)
6
-
6
+
7
7
  json.set! name, type == :binary ? value&.unpack1('H*') : value
8
8
  end
9
9
 
@@ -17,31 +17,25 @@ module StandardAPI
17
17
  preloads[key] = value
18
18
  when Hash, ActiveSupport::HashWithIndifferentAccess
19
19
  if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
20
- if !reflection.polymorphic?
21
- preloads[key] = preloadables_hash(reflection.klass, value)
22
- end
20
+ preloads[key.to_sym] = preloadables_hash(value)
23
21
  end
24
22
  end
25
23
  end
26
24
  end
27
25
 
28
- preloads.empty? ? record : record.preload(preloads)
26
+ preloads.present? ? record.preload(preloads) : record
29
27
  end
30
28
 
31
- def preloadables_hash(klass, iclds)
29
+ def preloadables_hash(iclds)
32
30
  preloads = {}
33
31
 
34
32
  iclds.each do |key, value|
35
- if reflection = klass.reflections[key]
36
- case value
37
- when true
38
- preloads[key] = value
39
- when Hash, ActiveSupport::HashWithIndifferentAccess
40
- if !value.keys.any? { |x| ['when', 'where', 'limit', 'offset', 'order', 'distinct'].include?(x) }
41
- if !reflection.polymorphic?
42
- preloads[key] = preloadables_hash(reflection.klass, value)
43
- end
44
- end
33
+ case value
34
+ when true
35
+ preloads[key] = value
36
+ when Hash, ActiveSupport::HashWithIndifferentAccess
37
+ if !value.keys.any? { |x| [ 'when', 'where', 'limit', 'offset', 'order', 'distinct' ].include?(x) }
38
+ preloads[key] = preloadables_hash(value)
45
39
  end
46
40
  end
47
41
  end
@@ -1,3 +1,3 @@
1
1
  module StandardAPI
2
- VERSION = '7.1.0'
2
+ VERSION = '7.1.1'
3
3
  end
@@ -31,11 +31,21 @@ includes.each do |inc, subinc|
31
31
  end
32
32
  else
33
33
  can_cache = can_cache_relation?(record, inc, subinc)
34
- if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
35
- can_cache = can_cache && !record.send(association.foreign_key).nil?
34
+ cache_key = nil
35
+
36
+ if can_cache
37
+ if association.is_a?(ActiveRecord::Reflection::BelongsToReflection)
38
+ can_cache = can_cache && !record.send(association.foreign_key).nil?
39
+ end
40
+
41
+ if can_cache
42
+ cache_key = association_cache_key(record, inc, subinc)
43
+ can_cache = cache_key.present?
44
+ end
36
45
  end
46
+
37
47
  json.set! inc do
38
- json.cache_if!(can_cache, can_cache ? association_cache_key(record, inc, subinc) : nil) do
48
+ json.cache_if!(can_cache, can_cache ? cache_key : nil) do
39
49
  value = record.send(inc)
40
50
  if value.nil?
41
51
  json.null!
@@ -112,7 +112,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
112
112
  @controller.params = {}
113
113
  assert_equal 'SELECT "references".* FROM "references" WHERE "references"."subject_id" = 1', @controller.send(:resources).to_sql
114
114
  end
115
-
115
+
116
116
  test "Auto includes on a controller without a model" do
117
117
  @controller = SessionsController.new
118
118
  assert_nil @controller.send(:model)
@@ -154,7 +154,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
154
154
  else
155
155
  assert_nil schema.dig('models', model.name, 'attributes', column.name, 'comment')
156
156
  end
157
-
157
+
158
158
  if column.respond_to?(:auto_populated?)
159
159
  assert_equal !!column.auto_populated?, schema.dig('models', model.name, 'attributes', column.name, 'auto_populated')
160
160
  end
@@ -263,7 +263,7 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
263
263
  assert JSON(response.body).has_key?('document')
264
264
  assert_nil JSON(response.body)['document']
265
265
  end
266
-
266
+
267
267
  test 'rendering serialize_attribute' do
268
268
  property = create(:property, description: 'This text will magically change')
269
269
  get property_path(property, format: 'json'), params: { id: property.id, magic: true }
@@ -274,15 +274,15 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
274
274
 
275
275
  test 'rendering an enum' do
276
276
  public_document = create(:document, level: 'public')
277
-
277
+
278
278
  get documents_path(format: 'json'), params: { limit: 1 }
279
279
  assert_equal JSON(response.body)[0]['level'], 'public'
280
-
280
+
281
281
  secret_document = create(:document, level: 'secret')
282
282
  get document_path(secret_document, format: 'json')
283
283
  assert_equal JSON(response.body)['level'], 'secret'
284
284
  end
285
-
285
+
286
286
  test '#index.json uses overridden partial' do
287
287
  create(:property, photos: [create(:photo)])
288
288
  get properties_path(format: 'json'), params: { limit: 100, include: [{:photos => { order: :id }}] }
@@ -726,4 +726,34 @@ class PropertiesControllerTest < ActionDispatch::IntegrationTest
726
726
  assert_equal [1], JSON(response.body)
727
727
  end
728
728
 
729
+ test 'preloading polymorphic associations' do
730
+ p1 = create(:property)
731
+ p2 = create(:property)
732
+ c1 = create(:camera)
733
+ c2 = create(:camera)
734
+ a1 = create(:account, subject: p1, subject_cached_at: Time.now)
735
+ a2 = create(:account, subject: p2, subject_cached_at: Time.now)
736
+ a3 = create(:account, subject: c1, subject_cached_at: Time.now)
737
+ a4 = create(:account, subject: c2, subject_cached_at: Time.now)
738
+ a5 = create(:account, subject: c2, subject_cached_at: Time.now)
739
+
740
+ assert_sql(
741
+ 'SELECT "properties".* FROM "properties" WHERE "properties"."id" IN ($1, $2)',
742
+ 'SELECT "cameras".* FROM "cameras" WHERE "cameras"."id" IN ($1, $2)'
743
+ ) do
744
+ assert_no_sql("SELECT \"properties\".* FROM \"properties\" WHERE \"properties\".\"id\" = $1 LIMIT $2") do
745
+ get accounts_path(limit: 10, include: { subject: { landlord: { when: { subject_type: 'Property' } } } }, format: 'json')
746
+
747
+ assert_equal p1.id, a1.subject_id
748
+ assert_equal p2.id, a2.subject_id
749
+ assert_equal c1.id, a3.subject_id
750
+ assert_equal p1.id, JSON(response.body).dig(0, 'subject', 'id')
751
+ assert_equal p2.id, JSON(response.body).dig(1, 'subject', 'id')
752
+ assert_equal c1.id, JSON(response.body).dig(2, 'subject', 'id')
753
+ assert_equal c2.id, JSON(response.body).dig(3, 'subject', 'id')
754
+ assert_equal c2.id, JSON(response.body).dig(4, 'subject', 'id')
755
+ end
756
+ end
757
+ end
758
+
729
759
  end
@@ -9,7 +9,11 @@ module AccountACL
9
9
  end
10
10
 
11
11
  def includes
12
- [ "photos", "subject", "property" ]
12
+ {
13
+ photos: true,
14
+ subject: [ 'landlord' ],
15
+ property: true
16
+ }
13
17
  end
14
18
 
15
19
  end
@@ -53,6 +53,7 @@ Rails.application.routes.draw do
53
53
  end
54
54
 
55
55
  standard_resource :account
56
+ standard_resources :accounts, only: :index
56
57
  # standard_resources :photos, only: [ :index, :show ]
57
58
 
58
59
  end
@@ -85,16 +85,58 @@ class ActiveSupport::TestCase
85
85
  { :controller => controller_path, :action => action }.merge(options)
86
86
  end
87
87
 
88
- def assert_sql(sql, &block)
89
- queries = []
90
- callback = -> (*, payload) do
91
- queries << payload[:sql]
88
+ def assert_sql(*expected)
89
+ return_value = nil
90
+
91
+ queries_ran = if block_given?
92
+ queries_ran = SQLLogger.log.size
93
+ return_value = yield if block_given?
94
+ SQLLogger.log[queries_ran...]
95
+ else
96
+ [expected.pop]
97
+ end
98
+
99
+ failed_patterns = []
100
+ expected.each do |pattern|
101
+ failed_patterns << pattern unless queries_ran.any?{ |sql| sql_equal(pattern, sql) }
92
102
  end
93
103
 
94
- ActiveSupport::Notifications.subscribed(callback, "sql.active_record", &block)
104
+ assert failed_patterns.empty?, <<~MSG
105
+ Query pattern(s) not found:
106
+ - #{failed_patterns.map{|l| l.gsub(/\n\s*/, " ")}.join('\n - ')}
107
+ Queries Ran (queries_ran.size):
108
+ - #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
109
+ MSG
95
110
 
96
- assert_not_nil queries.map { |x| x.strip.gsub(/\s+/, ' ') }.
97
- find { |x| x == sql.strip.gsub(/\s+/, ' ') }
111
+ return_value
112
+ end
113
+
114
+ def assert_no_sql(*not_expected)
115
+ return_value = nil
116
+ queries_ran = block_given? ? SQLLogger.log.size : 0
117
+ return_value = yield if block_given?
118
+ ensure
119
+ failed_patterns = []
120
+ queries_ran = SQLLogger.log[queries_ran...]
121
+ not_expected.each do |pattern|
122
+ failed_patterns << pattern if queries_ran.any?{ |sql| sql_equal(pattern, sql) }
123
+ end
124
+ assert failed_patterns.empty?, <<~MSG
125
+ Unexpected Query pattern(s) found:
126
+ - #{failed_patterns.map(&:inspect).join('\n - ')}
127
+ Queries Ran (queries_ran.size):
128
+ - #{queries_ran.map{|l| l.gsub(/\n\s*/, "\n ")}.join("\n - ")}
129
+ MSG
130
+
131
+ return_value
132
+ end
133
+ def sql_equal(expected, sql)
134
+ sql = sql.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/\s+/, ' ')
135
+ if expected.is_a?(String)
136
+ expected = Regexp.new(Regexp.escape(expected.strip.gsub(/"(\w+)"/, '\1').gsub(/\(\s+/, '(').gsub(/\s+\)/, ')').gsub(/\s+/, ' ')), Regexp::IGNORECASE)
137
+ end
138
+
139
+ expected.match(sql)
98
140
  end
99
141
 
100
142
  def assert_rendered(options = {}, message = nil)
@@ -234,6 +276,48 @@ class ActiveSupport::TestCase
234
276
  end
235
277
  end
236
278
 
279
+ class SQLLogger
280
+ class << self
281
+ attr_accessor :ignored_sql, :log, :log_all
282
+ def clear_log; self.log = []; self.log_all = []; end
283
+ end
284
+
285
+ self.clear_log
286
+
287
+ self.ignored_sql = [/^PRAGMA/i, /^SELECT currval/i, /^SELECT CAST/i, /^SELECT @@IDENTITY/i, /^SELECT @@ROWCOUNT/i, /^SAVEPOINT/i, /^ROLLBACK TO SAVEPOINT/i, /^RELEASE SAVEPOINT/i, /^SHOW max_identifier_length/i, /^BEGIN/i, /^COMMIT/i]
288
+
289
+ # FIXME: this needs to be refactored so specific database can add their own
290
+ # ignored SQL, or better yet, use a different notification for the queries
291
+ # instead examining the SQL content.
292
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im]
293
+ mysql_ignored = [/^SHOW FULL TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /, /^\s*SELECT (?:column_name|table_name)\b.*\bFROM information_schema\.(?:key_column_usage|tables)\b/im]
294
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
295
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im, /^\s*SELECT sql\b.*\bFROM sqlite_master/im]
296
+
297
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
298
+ ignored_sql.concat db_ignored_sql
299
+ end
300
+
301
+ attr_reader :ignore
302
+
303
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
304
+ @ignore = ignore
305
+ end
306
+
307
+ def call(name, start, finish, message_id, values)
308
+ sql = values[:sql]
309
+
310
+ # FIXME: this seems bad. we should probably have a better way to indicate
311
+ # the query was cached
312
+ return if 'CACHE' == values[:name]
313
+
314
+ self.class.log_all << sql
315
+ # puts sql
316
+ self.class.log << sql unless ignore =~ sql
317
+ end
318
+ end
319
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLLogger.new)
320
+
237
321
  end
238
322
 
239
323
  class ActionController::TestCase
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standardapi
3
3
  version: !ruby/object:Gem::Version
4
- version: 7.1.0
4
+ version: 7.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Bracy
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-04-30 00:00:00.000000000 Z
11
+ date: 2024-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails