dbwatcher 1.1.4 → 1.1.6

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 +80 -26
  3. data/app/assets/config/dbwatcher_manifest.js +9 -0
  4. data/app/assets/images/dbwatcher/README.md +24 -0
  5. data/app/assets/images/dbwatcher/apple-touch-icon.png +0 -0
  6. data/app/assets/images/dbwatcher/dbwatcher-tranparent_512x512.png +0 -0
  7. data/app/assets/images/dbwatcher/favicon-96x96.png +0 -0
  8. data/app/assets/images/dbwatcher/favicon.ico +0 -0
  9. data/app/assets/images/dbwatcher/site.webmanifest +21 -0
  10. data/app/assets/images/dbwatcher/unused-assets.zip +0 -0
  11. data/app/assets/images/dbwatcher/web-app-manifest-192x192.png +0 -0
  12. data/app/assets/images/dbwatcher/web-app-manifest-512x512.png +0 -0
  13. data/app/assets/stylesheets/dbwatcher/application.css +38 -4
  14. data/app/assets/stylesheets/dbwatcher/components/_tabulator.scss +57 -13
  15. data/app/controllers/dbwatcher/api/v1/system_info_controller.rb +1 -1
  16. data/app/controllers/dbwatcher/dashboard_controller.rb +1 -1
  17. data/app/views/dbwatcher/dashboard/_overview.html.erb +8 -7
  18. data/app/views/dbwatcher/sessions/index.html.erb +42 -59
  19. data/app/views/layouts/dbwatcher/application.html.erb +22 -6
  20. data/lib/dbwatcher/configuration.rb +49 -83
  21. data/lib/dbwatcher/logging.rb +2 -2
  22. data/lib/dbwatcher/services/diagram_analyzers/concerns/activerecord_introspection.rb +60 -0
  23. data/lib/dbwatcher/services/diagram_analyzers/concerns/association_scope_filtering.rb +60 -0
  24. data/lib/dbwatcher/services/diagram_analyzers/model_analysis/association_extractor.rb +224 -0
  25. data/lib/dbwatcher/services/diagram_analyzers/model_analysis/dataset_builder.rb +226 -0
  26. data/lib/dbwatcher/services/diagram_analyzers/model_analysis/model_discovery.rb +161 -0
  27. data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +27 -514
  28. data/lib/dbwatcher/services/diagram_data/attribute.rb +22 -83
  29. data/lib/dbwatcher/services/diagram_data/base.rb +129 -0
  30. data/lib/dbwatcher/services/diagram_data/entity.rb +23 -72
  31. data/lib/dbwatcher/services/diagram_data/relationship.rb +15 -66
  32. data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +2 -2
  33. data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +4 -14
  34. data/lib/dbwatcher/services/system_info/runtime_info_collector.rb +7 -7
  35. data/lib/dbwatcher/services/system_info/system_info_collector.rb +3 -3
  36. data/lib/dbwatcher/services/timeline_data_service/entry_builder.rb +23 -1
  37. data/lib/dbwatcher/storage/session_storage.rb +2 -2
  38. data/lib/dbwatcher/storage.rb +1 -1
  39. data/lib/dbwatcher/version.rb +1 -1
  40. metadata +17 -2
@@ -41,46 +41,39 @@
41
41
  </div>
42
42
 
43
43
  <!-- Content Area -->
44
- <div class="flex-1 overflow-auto p-4">
45
-
46
- <% if @sessions.empty? %>
47
- <div class="flex items-center justify-center h-full text-gray-500">
48
- <div class="text-center">
49
- <svg class="mx-auto h-8 w-8 text-gray-400 mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1"
51
- d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"/>
52
- </svg>
53
- <p class="text-xs">No tracking sessions yet</p>
54
- <p class="text-xs text-gray-400">Start tracking with <code class="bg-gray-200 px-1 rounded">Dbwatcher.track { ... }</code></p>
44
+ <div class="flex-1 overflow-auto">
45
+ <% if @sessions.empty? %>
46
+ <div class="p-8 text-center text-gray-500">
47
+ <%= image_tag "dbwatcher/dbwatcher-tranparent_512x512.png",
48
+ alt: "DBWatcher Logo",
49
+ class: "mx-auto w-12 h-12 opacity-30 mb-3" %>
50
+ <p class="text-sm font-medium text-gray-600 mb-2">No tracking sessions yet</p>
51
+ <p class="text-xs text-gray-400">Start tracking with <code class="bg-gray-100 px-1 rounded text-xs">Dbwatcher.track { ... }</code></p>
52
+ <p class="text-xs text-gray-400 mt-1">or add <code class="bg-gray-100 px-1 rounded text-xs">?dbwatch=true</code> to any URL</p>
55
53
  </div>
56
- </div>
57
- <% else %>
58
- <div class="bg-white border border-gray-300 rounded shadow-sm">
59
- <table class="compact-table sessions-table w-full">
54
+ <% else %>
55
+ <table class="compact-table w-full">
60
56
  <thead>
61
57
  <tr>
62
- <th class="text-left" style="min-width:180px; max-width:260px; width:18%">Session ID</th>
63
- <th class="text-left" style="min-width:160px; max-width:260px; width:22%">Name</th>
64
- <th class="text-center" style="width:100px">Status</th>
65
- <th class="text-center" style="width:100px">Changes</th>
66
- <th class="text-right" style="width:120px">Started</th>
67
- <th class="text-right" style="width:120px">Duration</th>
68
- <th class="text-center" style="width:80px">Actions</th>
58
+ <th class="text-left" style="width: 18%; min-width: 180px">Session ID</th>
59
+ <th class="text-left" style="width: 22%; min-width: 160px">Name</th>
60
+ <th class="text-center" style="width: 100px">Status</th>
61
+ <th class="text-center" style="width: 100px">Changes</th>
62
+ <th class="text-right" style="width: 120px">Started</th>
63
+ <th class="text-right" style="width: 120px">Duration</th>
64
+ <th class="text-center" style="width: 80px">Actions</th>
69
65
  </tr>
70
66
  </thead>
71
67
  <tbody>
72
68
  <% @sessions.each do |session| %>
73
69
  <tr class="session-row">
74
- <td style="min-width:180px; max-width:260px; width:18%">
75
- <span class="font-mono text-xs">
76
- <%= safe_value(session, :id) %>
77
- </span>
70
+ <td class="font-mono text-xs">
71
+ <%= safe_value(session, :id) %>
78
72
  </td>
79
- <td style="min-width:160px; max-width:260px; width:22%" title="<%= safe_value(session, :name) %>">
73
+ <td title="<%= safe_value(session, :name) %>">
80
74
  <%= link_to display_session_name(safe_value(session, :name)),
81
75
  session_path(safe_value(session, :id)),
82
- class: "text-navy-dark hover:text-blue-medium whitespace-normal break-words inline-block",
83
- style: "max-width:260px; overflow-x:auto; display:inline-block;" %>
76
+ class: "text-navy-dark hover:text-blue-medium" %>
84
77
  </td>
85
78
  <td class="text-center">
86
79
  <%= render 'dbwatcher/shared/badge',
@@ -106,44 +99,34 @@
106
99
  ) rescue 'N/A' %>
107
100
  <% end %>
108
101
  </td>
109
- <td class="text-center actions-cell">
110
- <div class="flex gap-1 justify-end">
111
- <%= link_to session_path(safe_value(session, :id)),
112
- class: "compact-button bg-navy-dark text-white hover:bg-blue-medium",
113
- title: "View session details" do %>
114
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
115
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
116
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
117
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
118
- d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
119
- </svg>
120
- <% end %>
121
- </div>
102
+ <td class="text-center">
103
+ <%= link_to "View", session_path(safe_value(session, :id)),
104
+ class: "compact-button bg-navy-dark text-white hover:bg-blue-medium",
105
+ title: "View session details" %>
122
106
  </td>
123
107
  </tr>
124
108
  <% end %>
125
109
  </tbody>
126
110
  </table>
127
- </div>
128
111
 
129
- <!-- No Results Message -->
130
- <div
131
- x-show="filterText && document.querySelectorAll('.session-row:not(.hidden)').length === 0"
132
- x-cloak
133
- class="mt-4 text-center py-8 bg-gray-50 border border-gray-200 rounded">
134
- <p class="text-gray-500">No sessions match your filter criteria</p>
135
- <button
136
- @click="filterText = ''"
137
- class="mt-2 text-blue-medium hover:text-blue-700 text-sm">
138
- Clear filter
139
- </button>
140
- </div>
141
- <% end %>
112
+ <!-- No Results Message -->
113
+ <div
114
+ x-show="filterText && document.querySelectorAll('.session-row:not(.hidden)').length === 0"
115
+ x-cloak
116
+ class="p-8 text-center text-gray-500 bg-gray-50">
117
+ <p class="mb-2">No sessions match your filter criteria</p>
118
+ <button
119
+ @click="filterText = ''"
120
+ class="compact-button bg-gray-500 text-white hover:bg-gray-600">
121
+ Clear filter
122
+ </button>
123
+ </div>
124
+ <% end %>
125
+ </div>
142
126
 
143
127
  <!-- Status Bar -->
144
- <div class="h-6 bg-gray-100 border-t border-gray-300 flex items-center px-4 text-xs text-gray-600 mt-4">
145
- <span x-show="!filterText"><%= @sessions.count %> sessions total •
146
- <%= @sessions.count { |s| session_active?(s) } %> active</span>
128
+ <div class="h-6 bg-gray-100 border-t border-gray-300 flex items-center px-4 text-xs text-gray-600">
129
+ <span x-show="!filterText"><%= @sessions.count %> sessions total • <%= @sessions.count { |s| session_active?(s) } %> active</span>
147
130
  <span x-show="filterText" x-text="`${document.querySelectorAll('.session-row:not(.hidden)').length} of ${<%= @sessions.count %>} sessions shown`"></span>
148
131
  <span class="ml-auto">Last updated: <%= Time.current.strftime("%H:%M:%S") %></span>
149
132
  </div>
@@ -5,6 +5,14 @@
5
5
  <meta name="viewport" content="width=device-width,initial-scale=1">
6
6
  <%= csrf_meta_tags %>
7
7
 
8
+ <!-- Favicon -->
9
+ <%= favicon_link_tag asset_path('dbwatcher/favicon.ico') %>
10
+ <%= favicon_link_tag asset_path('dbwatcher/favicon-96x96.png'), sizes: '96x96', type: 'image/png' %>
11
+ <%= favicon_link_tag asset_path('dbwatcher/apple-touch-icon.png'), rel: 'apple-touch-icon', sizes: '180x180' %>
12
+ <%= favicon_link_tag asset_path('dbwatcher/web-app-manifest-192x192.png'), sizes: '192x192', type: 'image/png' %>
13
+ <%= favicon_link_tag asset_path('dbwatcher/web-app-manifest-512x512.png'), sizes: '512x512', type: 'image/png' %>
14
+ <link rel="manifest" href="<%= asset_path('dbwatcher/site.webmanifest') %>"
15
+
8
16
  <!-- Alpine.js and Plugins - ensure plugins load BEFORE Alpine.js core -->
9
17
  <script defer src="https://unpkg.com/@alpinejs/collapse@3.x.x/dist/cdn.min.js"></script>
10
18
  <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
@@ -119,12 +127,20 @@
119
127
  :style="{ width: sidebarCollapsed ? '48px' : sidebarWidth + 'px' }">
120
128
  <div class="flex flex-col h-full">
121
129
  <!-- Logo -->
122
- <div class="h-10 flex items-center justify-between px-3 border-b border-gray-700">
123
- <div class="flex items-center gap-2" x-show="!sidebarCollapsed">
124
- <svg class="w-5 h-5 text-gold-light" fill="none" stroke="currentColor" viewBox="0 0 24 24">
125
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
126
- </svg>
127
- <span class="text-sm font-medium text-white">DB Watcher</span>
130
+ <div class="h-12 flex items-center justify-between px-3 border-b border-gray-700 bg-gray-800">
131
+ <div class="flex items-center gap-3" x-show="!sidebarCollapsed">
132
+ <%= image_tag "dbwatcher/dbwatcher-tranparent_512x512.png",
133
+ alt: "DBWatcher Logo",
134
+ class: "w-10 h-10" %>
135
+ <div class="flex flex-col">
136
+ <span class="text-base font-bold text-white">dbwatcher</span>
137
+ </div>
138
+ </div>
139
+ <!-- Collapsed state logo -->
140
+ <div class="flex items-center justify-center w-full" x-show="sidebarCollapsed">
141
+ <%= image_tag "dbwatcher/dbwatcher-tranparent_512x512.png",
142
+ alt: "dbwatcher Logo",
143
+ class: "w-8 h-8" %>
128
144
  </div>
129
145
  <button @click="sidebarCollapsed = !sidebarCollapsed"
130
146
  class="p-1 hover:bg-gray-800 rounded text-gray-400">
@@ -3,104 +3,84 @@
3
3
  module Dbwatcher
4
4
  # Configuration class for DBWatcher
5
5
  #
6
- # This class manages all configuration options for DBWatcher, including
7
- # storage, tracking, and diagram visualization settings.
6
+ # Simplified configuration with logical groupings and sensible defaults
8
7
  class Configuration
9
- # Storage configuration
10
- attr_accessor :storage_path, :enabled, :max_sessions, :auto_clean_after_days
8
+ # Core settings - what users need most
9
+ attr_accessor :enabled, :storage_path
11
10
 
12
- # Query tracking configuration
13
- attr_accessor :track_queries, :slow_query_threshold, :max_query_logs_per_day
11
+ # Session management - how data is stored and cleaned
12
+ attr_accessor :max_sessions, :auto_clean_days
14
13
 
15
- # Routing configuration
16
- attr_accessor :mount_path
14
+ # Query tracking - performance monitoring
15
+ attr_accessor :track_queries
17
16
 
18
- # Diagram configuration
19
- attr_accessor :diagram_show_attributes, :diagram_show_methods, :diagram_show_cardinality,
20
- :diagram_max_attributes, :diagram_attribute_types, :diagram_relationship_labels,
21
- :diagram_preserve_table_case, :diagram_direction, :diagram_cardinality_format,
22
- :diagram_show_attribute_count, :diagram_show_method_count
17
+ # System info - debugging and monitoring
18
+ attr_accessor :system_info, :debug_mode
23
19
 
24
- # System information configuration
25
- attr_accessor :collect_system_info, :system_info_refresh_interval,
26
- :collect_sensitive_env_vars, :system_info_cache_duration,
27
- :system_info_include_performance_metrics
28
-
29
- # Logging configuration
30
- attr_accessor :debug_logging
20
+ # Advanced diagram options - available but not commonly needed
21
+ attr_accessor :diagram_show_methods, :diagram_max_attributes,
22
+ :diagram_attribute_types, :diagram_relationship_labels,
23
+ :diagram_show_attributes, :diagram_show_cardinality
31
24
 
32
25
  # Initialize with default values
33
26
  def initialize
34
- # Storage configuration defaults
35
- @storage_path = default_storage_path
27
+ # Core settings
36
28
  @enabled = true
29
+ @storage_path = default_storage_path
30
+
31
+ # Session management
37
32
  @max_sessions = 50
38
- @auto_clean_after_days = 7
33
+ @auto_clean_days = 7
39
34
 
40
- # Query tracking configuration defaults
35
+ # Query tracking
41
36
  @track_queries = false
42
- @slow_query_threshold = 200 # milliseconds
43
- @max_query_logs_per_day = 1000
44
-
45
- # Routing configuration defaults
46
- @mount_path = "/dbwatcher"
47
37
 
48
- # Initialize diagram configuration with defaults
49
- initialize_diagram_config
38
+ # System info
39
+ @system_info = true
40
+ @debug_mode = false
50
41
 
51
- # Initialize system information configuration with defaults
52
- initialize_system_info_config
42
+ # Advanced diagram options - sensible defaults
43
+ @diagram_show_methods = false
44
+ @diagram_max_attributes = 10
45
+ @diagram_attribute_types = true
46
+ @diagram_relationship_labels = true
47
+ @diagram_show_attributes = true
48
+ @diagram_show_cardinality = true
49
+ end
53
50
 
54
- # Initialize logging configuration with defaults
55
- initialize_logging_config
51
+ # Fixed defaults for options that are still used in codebase but not configurable
52
+ def slow_query_threshold
53
+ 200 # Fixed default value
56
54
  end
57
55
 
58
- # Initialize diagram configuration with default values
59
- def initialize_diagram_config
60
- @diagram_show_attributes = true
61
- @diagram_show_methods = false # Hide methods by default
62
- @diagram_show_cardinality = true
63
- @diagram_max_attributes = 10
64
- @diagram_attribute_types = true # Changed from array to boolean
65
- @diagram_relationship_labels = true # Changed from symbol to boolean
66
- @diagram_preserve_table_case = false # Changed from true to false
67
- @diagram_direction = "LR" # Left to right by default
68
- @diagram_cardinality_format = :simple # Use simpler 1:N format
69
- @diagram_show_attribute_count = true
70
- @diagram_show_method_count = true
56
+ def diagram_direction
57
+ "LR" # Fixed default value
71
58
  end
72
59
 
73
- # Initialize system information configuration with default values
74
- def initialize_system_info_config
75
- @collect_system_info = true
76
- @system_info_refresh_interval = 5 * 60 # 5 minutes in seconds
77
- @collect_sensitive_env_vars = false
78
- @system_info_cache_duration = 60 * 60 # 1 hour in seconds
79
- @system_info_include_performance_metrics = true
60
+ # Fixed defaults for complex options that are still used in codebase but not configurable
61
+ def max_query_logs_per_day = 1000
62
+ def system_info_refresh_interval = 5 * 60
63
+ def system_info_cache_duration = 60 * 60
64
+
65
+ def collect_sensitive_env_vars?
66
+ false
80
67
  end
81
68
 
82
- # Initialize logging configuration with default values
83
- def initialize_logging_config
84
- @debug_logging = false
69
+ def system_info_include_performance_metrics?
70
+ true
85
71
  end
86
72
 
87
73
  # Validate configuration
88
- #
89
- # @return [Boolean] true if configuration is valid
90
74
  def valid?
91
75
  validate_storage_path
92
76
  validate_max_sessions
93
- validate_auto_clean_after_days
94
- validate_slow_query_threshold
95
- validate_max_query_logs_per_day
77
+ validate_auto_clean_days
96
78
  true
97
79
  end
98
80
 
99
81
  private
100
82
 
101
83
  # Default storage path based on Rails or current directory
102
- #
103
- # @return [String] default storage path
104
84
  def default_storage_path
105
85
  if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
106
86
  Rails.root.join("tmp", "dbwatcher").to_s
@@ -127,25 +107,11 @@ module Dbwatcher
127
107
  raise ConfigurationError, "max_sessions must be a positive integer"
128
108
  end
129
109
 
130
- # Validate auto clean after days
131
- def validate_auto_clean_after_days
132
- return if auto_clean_after_days.is_a?(Integer) && auto_clean_after_days.positive?
133
-
134
- raise ConfigurationError, "auto_clean_after_days must be a positive integer"
135
- end
136
-
137
- # Validate slow query threshold
138
- def validate_slow_query_threshold
139
- return if slow_query_threshold.is_a?(Integer) && slow_query_threshold.positive?
140
-
141
- raise ConfigurationError, "slow_query_threshold must be a positive integer"
142
- end
143
-
144
- # Validate max query logs per day
145
- def validate_max_query_logs_per_day
146
- return if max_query_logs_per_day.is_a?(Integer) && max_query_logs_per_day.positive?
110
+ # Validate auto clean days
111
+ def validate_auto_clean_days
112
+ return if auto_clean_days.is_a?(Integer) && auto_clean_days.positive?
147
113
 
148
- raise ConfigurationError, "max_query_logs_per_day must be a positive integer"
114
+ raise ConfigurationError, "auto_clean_days must be a positive integer"
149
115
  end
150
116
  end
151
117
 
@@ -40,8 +40,8 @@ module Dbwatcher
40
40
  # Check if debug logging is enabled
41
41
  # @return [Boolean] true if debug logging is enabled
42
42
  def debug_enabled?
43
- return Dbwatcher.configuration.debug_logging if defined?(Dbwatcher.configuration) &&
44
- Dbwatcher.configuration.respond_to?(:debug_logging)
43
+ return Dbwatcher.configuration.debug_mode if defined?(Dbwatcher.configuration) &&
44
+ Dbwatcher.configuration.respond_to?(:debug_mode)
45
45
  return Rails.env.development? if defined?(Rails)
46
46
 
47
47
  false
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module DiagramAnalyzers
6
+ module Concerns
7
+ # Concern for ActiveRecord introspection utilities
8
+ #
9
+ # This module provides common methods for checking ActiveRecord availability
10
+ # and performing model introspection operations.
11
+ module ActiverecordIntrospection
12
+ extend ActiveSupport::Concern if defined?(ActiveSupport)
13
+
14
+ private
15
+
16
+ # Check if ActiveRecord is available
17
+ #
18
+ # @return [Boolean]
19
+ def activerecord_available?
20
+ defined?(ActiveRecord::Base)
21
+ end
22
+
23
+ # Check if models analysis is available
24
+ #
25
+ # @return [Boolean] true if models can be analyzed
26
+ def models_available?
27
+ unless activerecord_available?
28
+ Rails.logger.warn "#{self.class.name}: ActiveRecord not available"
29
+ return false
30
+ end
31
+
32
+ true
33
+ end
34
+
35
+ # Eagerly load all models including those from gems
36
+ #
37
+ # @return [void]
38
+ def eager_load_models
39
+ return unless defined?(Rails) && Rails.respond_to?(:application)
40
+
41
+ begin
42
+ # Force eager loading of application models
43
+ Rails.application.eager_load!
44
+
45
+ # Also load models from engines/gems if any are configured
46
+ Rails::Engine.descendants.each do |engine|
47
+ engine.eager_load! if engine.respond_to?(:eager_load!)
48
+ rescue StandardError => e
49
+ error_message = "#{self.class.name}: Could not eager load engine #{engine.class.name}: #{e.message}"
50
+ Rails.logger.debug error_message
51
+ end
52
+ rescue StandardError => e
53
+ Rails.logger.debug "#{self.class.name}: Could not eager load models: #{e.message}"
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module DiagramAnalyzers
6
+ module Concerns
7
+ # Concern for filtering associations based on scope
8
+ #
9
+ # This module provides methods for determining which associations
10
+ # should be included in the analysis based on session context.
11
+ module AssociationScopeFiltering
12
+ extend ActiveSupport::Concern if defined?(ActiveSupport)
13
+
14
+ private
15
+
16
+ # Check if target model is in analysis scope
17
+ #
18
+ # @param association [Object] association object
19
+ # @param session_tables [Array<String>] tables from session context
20
+ # @return [Boolean] true if target model should be included
21
+ def target_model_in_scope?(association, session_tables = [])
22
+ target_table = get_association_table_name(association)
23
+
24
+ # If analyzing session, both tables must be in session
25
+ # If analyzing globally, include all
26
+ return true if session_tables.empty?
27
+
28
+ # Skip if target table is not in session
29
+ return false if target_table && !session_tables.include?(target_table)
30
+
31
+ true
32
+ end
33
+
34
+ # Get table name for association target
35
+ #
36
+ # @param association [Object] association object
37
+ # @return [String, nil] table name
38
+ def get_association_table_name(association)
39
+ association.table_name
40
+ rescue StandardError => e
41
+ Rails.logger.warn "#{self.class.name}: Could not get table name for #{association.name}: #{e.message}"
42
+ nil
43
+ end
44
+
45
+ # Extract tables that were involved in the session
46
+ #
47
+ # @param session [Object] session object with changes
48
+ # @return [Array<String>] unique table names
49
+ def extract_session_tables(session)
50
+ return [] unless session&.changes
51
+
52
+ session.changes.map do |change|
53
+ change[:table_name] || change["table_name"]
54
+ end.compact.uniq
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end