standardapi 7.1.0 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
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