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
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "base"
4
+
3
5
  module Dbwatcher
4
6
  module Services
5
7
  module DiagramData
@@ -18,7 +20,7 @@ module Dbwatcher
18
20
  # )
19
21
  # attribute.valid? # => true
20
22
  # attribute.to_h # => { name: "email", type: "string", ... }
21
- class Attribute
23
+ class Attribute < Base
22
24
  attr_accessor :name, :type, :nullable, :default, :metadata
23
25
 
24
26
  # Initialize attribute
@@ -29,6 +31,7 @@ module Dbwatcher
29
31
  # @param default [Object] default value
30
32
  # @param metadata [Hash] additional type-specific information
31
33
  def initialize(name:, type: nil, nullable: true, default: nil, metadata: {})
34
+ super() # Initialize parent class
32
35
  @name = name.to_s
33
36
  @type = type.to_s
34
37
  @nullable = nullable == true
@@ -36,16 +39,23 @@ module Dbwatcher
36
39
  @metadata = metadata.is_a?(Hash) ? metadata : {}
37
40
  end
38
41
 
39
- # Check if attribute is valid
40
- #
41
- # @return [Boolean] true if attribute has required fields
42
- def valid?
43
- validation_errors.empty?
42
+ # Implementation for Base class
43
+ def comparable_attributes
44
+ [name, type, nullable, default, metadata]
44
45
  end
45
46
 
46
- # Get validation errors
47
- #
48
- # @return [Array<String>] array of validation error messages
47
+ # Implementation for Base class
48
+ def serializable_attributes
49
+ {
50
+ name: name,
51
+ type: type,
52
+ nullable: nullable,
53
+ default: default,
54
+ metadata: metadata
55
+ }
56
+ end
57
+
58
+ # Implementation for Base class
49
59
  def validation_errors
50
60
  errors = []
51
61
  errors << "Name cannot be blank" if name.nil? || name.to_s.strip.empty?
@@ -67,86 +77,15 @@ module Dbwatcher
67
77
  metadata[:foreign_key] == true || name.to_s.end_with?("_id")
68
78
  end
69
79
 
70
- # Serialize attribute to hash
71
- #
72
- # @return [Hash] serialized attribute data
73
- def to_h
80
+ # Override base class method to handle nullable default
81
+ def self.extract_constructor_args(hash)
74
82
  {
75
- name: name,
76
- type: type,
77
- nullable: nullable,
78
- default: default,
79
- metadata: metadata
80
- }
81
- end
82
-
83
- # Serialize attribute to JSON
84
- #
85
- # @return [String] JSON representation
86
- def to_json(*args)
87
- to_h.to_json(*args)
88
- end
89
-
90
- # Create attribute from hash
91
- #
92
- # @param hash [Hash] attribute data
93
- # @return [Attribute] new attribute instance
94
- def self.from_h(hash)
95
- # Convert string keys to symbols for consistent access
96
- hash = hash.transform_keys(&:to_sym) if hash.keys.first.is_a?(String)
97
-
98
- # Use fetch with default values to handle missing fields
99
- new(
100
83
  name: hash[:name],
101
84
  type: hash[:type],
102
85
  nullable: hash.key?(:nullable) ? hash[:nullable] : true,
103
86
  default: hash[:default],
104
87
  metadata: hash[:metadata] || {}
105
- )
106
- end
107
-
108
- # Create attribute from JSON
109
- #
110
- # @param json [String] JSON string
111
- # @return [Attribute] new attribute instance
112
- def self.from_json(json)
113
- from_h(JSON.parse(json))
114
- end
115
-
116
- # Check equality with another attribute
117
- #
118
- # @param other [Attribute] other attribute to compare
119
- # @return [Boolean] true if attributes are equal
120
- def ==(other)
121
- return false unless other.is_a?(Attribute)
122
-
123
- name == other.name &&
124
- type == other.type &&
125
- nullable == other.nullable &&
126
- default == other.default &&
127
- metadata == other.metadata
128
- end
129
-
130
- # Generate hash code for attribute
131
- #
132
- # @return [Integer] hash code
133
- def hash
134
- [name, type, nullable, default, metadata].hash
135
- end
136
-
137
- # String representation of attribute
138
- #
139
- # @return [String] string representation
140
- def to_s
141
- "#{self.class.name}(name: #{name}, type: #{type}, nullable: #{nullable})"
142
- end
143
-
144
- # Detailed string representation
145
- #
146
- # @return [String] detailed string representation
147
- def inspect
148
- "#{self.class.name}(name: #{name.inspect}, type: #{type.inspect}, " \
149
- "nullable: #{nullable.inspect}, default: #{default.inspect}, metadata: #{metadata.inspect})"
88
+ }
150
89
  end
151
90
  end
152
91
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+
5
+ module Dbwatcher
6
+ module Services
7
+ module DiagramData
8
+ # Base class for diagram data objects
9
+ #
10
+ # Provides common functionality for serialization, validation, and comparison
11
+ # that is shared across Attribute, Entity, and Relationship classes.
12
+ #
13
+ # Subclasses must implement:
14
+ # - comparable_attributes: Array of values used for equality comparison
15
+ # - serializable_attributes: Hash of attributes for serialization
16
+ # - validation_errors: Array of validation error strings (optional)
17
+ #
18
+ # @example
19
+ # class MyClass < Base
20
+ # def comparable_attributes
21
+ # [name, type, value]
22
+ # end
23
+ #
24
+ # def serializable_attributes
25
+ # { name: name, type: type, value: value }
26
+ # end
27
+ # end
28
+ class Base
29
+ # Check if object is valid
30
+ #
31
+ # @return [Boolean] true if object has no validation errors
32
+ def valid?
33
+ validation_errors.empty?
34
+ end
35
+
36
+ # Check equality with another object of the same class
37
+ #
38
+ # @param other [Object] object to compare with
39
+ # @return [Boolean] true if objects are equal
40
+ def ==(other)
41
+ return false unless other.is_a?(self.class)
42
+
43
+ comparable_attributes == other.comparable_attributes
44
+ end
45
+
46
+ # Generate hash code for object
47
+ #
48
+ # @return [Integer] hash code
49
+ def hash
50
+ comparable_attributes.hash
51
+ end
52
+
53
+ # Serialize object to hash
54
+ #
55
+ # @return [Hash] serialized object data
56
+ def to_h
57
+ serializable_attributes
58
+ end
59
+
60
+ # Serialize object to JSON
61
+ #
62
+ # @return [String] JSON representation
63
+ def to_json(*args)
64
+ to_h.to_json(*args)
65
+ end
66
+
67
+ # Create object from hash
68
+ #
69
+ # @param hash [Hash] object data
70
+ # @return [Object] new object instance
71
+ def self.from_h(hash)
72
+ # Convert string keys to symbols for consistent access
73
+ hash = hash.transform_keys(&:to_sym) if hash.respond_to?(:transform_keys) && hash.keys.first.is_a?(String)
74
+
75
+ new(**extract_constructor_args(hash))
76
+ end
77
+
78
+ # Create object from JSON
79
+ #
80
+ # @param json [String] JSON string
81
+ # @return [Object] new object instance
82
+ def self.from_json(json)
83
+ from_h(JSON.parse(json))
84
+ end
85
+
86
+ # String representation of object
87
+ #
88
+ # @return [String] string representation
89
+ def to_s
90
+ attrs = serializable_attributes.map { |k, v| "#{k}: #{v}" }.join(", ")
91
+ "#{self.class.name}(#{attrs})"
92
+ end
93
+
94
+ # Detailed string representation
95
+ #
96
+ # @return [String] detailed string representation
97
+ def inspect
98
+ attrs = serializable_attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
99
+ "#{self.class.name}(#{attrs})"
100
+ end
101
+
102
+ # Default implementation - subclasses should override
103
+ def comparable_attributes
104
+ raise NotImplementedError, "#{self.class} must implement #comparable_attributes"
105
+ end
106
+
107
+ # Default implementation - subclasses should override
108
+ def serializable_attributes
109
+ raise NotImplementedError, "#{self.class} must implement #serializable_attributes"
110
+ end
111
+
112
+ # Default implementation - subclasses should override if validation needed
113
+ def validation_errors
114
+ []
115
+ end
116
+
117
+ # Extract constructor arguments from hash
118
+ # Subclasses can override this for custom initialization logic
119
+ #
120
+ # @param hash [Hash] object data
121
+ # @return [Hash] constructor arguments
122
+ def self.extract_constructor_args(hash)
123
+ hash
124
+ end
125
+ private_class_method :extract_constructor_args
126
+ end
127
+ end
128
+ end
129
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "base"
4
+
3
5
  module Dbwatcher
4
6
  module Services
5
7
  module DiagramData
@@ -20,7 +22,7 @@ module Dbwatcher
20
22
  # )
21
23
  # entity.valid? # => true
22
24
  # entity.to_h # => { id: "users", name: "User", ... }
23
- class Entity
25
+ class Entity < Base
24
26
  attr_accessor :id, :name, :type, :attributes, :metadata
25
27
 
26
28
  # Initialize entity
@@ -31,6 +33,7 @@ module Dbwatcher
31
33
  # @param attributes [Array<Attribute>] entity attributes/properties
32
34
  # @param metadata [Hash] additional type-specific information
33
35
  def initialize(id:, name:, type: "default", attributes: [], metadata: {})
36
+ super() # Initialize parent class
34
37
  @id = id.to_s
35
38
  @name = name.to_s
36
39
  @type = type.to_s
@@ -38,16 +41,23 @@ module Dbwatcher
38
41
  @metadata = metadata.is_a?(Hash) ? metadata : {}
39
42
  end
40
43
 
41
- # Check if entity is valid
42
- #
43
- # @return [Boolean] true if entity has required fields
44
- def valid?
45
- validation_errors.empty?
44
+ # Implementation for Base class
45
+ def comparable_attributes
46
+ [id, name, type, attributes, metadata]
46
47
  end
47
48
 
48
- # Get validation errors
49
- #
50
- # @return [Array<String>] array of validation error messages
49
+ # Implementation for Base class
50
+ def serializable_attributes
51
+ {
52
+ id: id,
53
+ name: name,
54
+ type: type,
55
+ attributes: attributes.map(&:to_h),
56
+ metadata: metadata
57
+ }
58
+ end
59
+
60
+ # Implementation for Base class
51
61
  def validation_errors
52
62
  errors = []
53
63
  errors << "ID cannot be blank" if id.nil? || id.to_s.strip.empty?
@@ -91,80 +101,21 @@ module Dbwatcher
91
101
  attributes.select(&:foreign_key?)
92
102
  end
93
103
 
94
- # Serialize entity to hash
95
- #
96
- # @return [Hash] serialized entity data
97
- def to_h
98
- {
99
- id: id,
100
- name: name,
101
- type: type,
102
- attributes: attributes.map(&:to_h),
103
- metadata: metadata
104
- }
105
- end
106
-
107
- # Serialize entity to JSON
108
- #
109
- # @return [String] JSON representation
110
- def to_json(*args)
111
- to_h.to_json(*args)
112
- end
113
-
114
- # Create entity from hash
115
- #
116
- # @param hash [Hash] entity data
117
- # @return [Entity] new entity instance
118
- def self.from_h(hash)
104
+ # Override base class method to handle attributes array
105
+ def self.extract_constructor_args(hash)
119
106
  attrs = []
120
107
  if hash[:attributes] || hash["attributes"]
121
108
  attr_data = hash[:attributes] || hash["attributes"]
122
109
  attrs = attr_data.map { |attr| Attribute.from_h(attr) }
123
110
  end
124
111
 
125
- new(
112
+ {
126
113
  id: hash[:id] || hash["id"],
127
114
  name: hash[:name] || hash["name"],
128
115
  type: hash[:type] || hash["type"] || "default",
129
116
  attributes: attrs,
130
117
  metadata: hash[:metadata] || hash["metadata"] || {}
131
- )
132
- end
133
-
134
- # Create entity from JSON
135
- #
136
- # @param json [String] JSON string
137
- # @return [Entity] new entity instance
138
- def self.from_json(json)
139
- from_h(JSON.parse(json))
140
- end
141
-
142
- # Check equality with another entity
143
- #
144
- # @param other [Entity] other entity to compare
145
- # @return [Boolean] true if entities are equal
146
- def ==(other)
147
- return false unless other.is_a?(Entity)
148
-
149
- id == other.id &&
150
- name == other.name &&
151
- type == other.type &&
152
- attributes == other.attributes &&
153
- metadata == other.metadata
154
- end
155
-
156
- # Generate hash code for entity
157
- #
158
- # @return [Integer] hash code
159
- def hash
160
- [id, name, type, attributes, metadata].hash
161
- end
162
-
163
- # String representation of entity
164
- #
165
- # @return [String] string representation
166
- def to_s
167
- "#{self.class.name}(id: #{id}, name: #{name}, type: #{type})"
118
+ }
168
119
  end
169
120
 
170
121
  # Detailed string representation
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "base"
3
4
  require_relative "relationship_params"
4
5
 
5
6
  module Dbwatcher
@@ -21,7 +22,7 @@ module Dbwatcher
21
22
  # )
22
23
  # relationship.valid? # => true
23
24
  # relationship.to_h # => { source_id: "users", target_id: "orders", ... }
24
- class Relationship
25
+ class Relationship < Base
25
26
  attr_accessor :source_id, :target_id, :type, :label, :cardinality, :metadata
26
27
 
27
28
  # Valid cardinality types
@@ -57,6 +58,7 @@ module Dbwatcher
57
58
  # @param params [RelationshipParams, Hash] relationship parameters
58
59
  # @return [Relationship] new relationship instance
59
60
  def initialize(params)
61
+ super() # Initialize parent class
60
62
  params = RelationshipParams.new(params) if params.is_a?(Hash)
61
63
 
62
64
  @source_id = params.source_id.to_s
@@ -108,10 +110,18 @@ module Dbwatcher
108
110
  ERD_NOTATIONS[infer_cardinality] || DEFAULT_ERD_NOTATION
109
111
  end
110
112
 
111
- # Serialize relationship to hash
112
- #
113
- # @return [Hash] serialized relationship data
114
- def to_h
113
+ # Override base class method to handle simple hash initialization
114
+ def self.extract_constructor_args(hash)
115
+ hash
116
+ end
117
+
118
+ # Implementation for Base class
119
+ def comparable_attributes
120
+ [source_id, target_id, type, label, cardinality, metadata]
121
+ end
122
+
123
+ # Implementation for Base class
124
+ def serializable_attributes
115
125
  {
116
126
  source_id: source_id,
117
127
  target_id: target_id,
@@ -121,67 +131,6 @@ module Dbwatcher
121
131
  metadata: metadata
122
132
  }
123
133
  end
124
-
125
- # Serialize relationship to JSON
126
- #
127
- # @return [String] JSON representation
128
- def to_json(*args)
129
- to_h.to_json(*args)
130
- end
131
-
132
- # Create relationship from hash
133
- #
134
- # @param hash [Hash] relationship data
135
- # @return [Relationship] new relationship instance
136
- def self.from_h(hash)
137
- hash = hash.transform_keys(&:to_sym) if hash.keys.first.is_a?(String)
138
- new(hash)
139
- end
140
-
141
- # Create relationship from JSON
142
- #
143
- # @param json [String] JSON string
144
- # @return [Relationship] new relationship instance
145
- def self.from_json(json)
146
- from_h(JSON.parse(json))
147
- end
148
-
149
- # Check equality with another relationship
150
- #
151
- # @param other [Relationship] other relationship to compare
152
- # @return [Boolean] true if relationships are equal
153
- def ==(other)
154
- return false unless other.is_a?(Relationship)
155
-
156
- source_id == other.source_id &&
157
- target_id == other.target_id &&
158
- type == other.type &&
159
- label == other.label &&
160
- cardinality == other.cardinality &&
161
- metadata == other.metadata
162
- end
163
-
164
- # Generate hash code for relationship
165
- #
166
- # @return [Integer] hash code
167
- def hash
168
- [source_id, target_id, type, label, cardinality, metadata].hash
169
- end
170
-
171
- # String representation of relationship
172
- #
173
- # @return [String] string representation
174
- def to_s
175
- "#{self.class.name}(source: #{source_id}, target: #{target_id}, type: #{type})"
176
- end
177
-
178
- # Detailed string representation
179
- #
180
- # @return [String] detailed string representation
181
- def inspect
182
- "#{self.class.name}(source: #{source_id.inspect}, target: #{target_id.inspect}, " \
183
- "type: #{type.inspect}, label: #{label.inspect}, cardinality: #{cardinality.inspect})"
184
- end
185
134
  end
186
135
  end
187
136
  end
@@ -70,7 +70,7 @@ module Dbwatcher
70
70
  # @param entity [DiagramData::Entity] entity to render
71
71
  # @return [Array<String>] entity definition lines
72
72
  def build_erd_entity(entity)
73
- table_name = Sanitizer.table_name(entity.name, preserve_table_case?)
73
+ table_name = Sanitizer.table_name(entity.name)
74
74
  lines = [" #{table_name} {"]
75
75
 
76
76
  # Add attributes if enabled and available
@@ -92,7 +92,7 @@ module Dbwatcher
92
92
  # @return [String] formatted entity name
93
93
  def format_entity_name(entity_id, dataset)
94
94
  entity_name = dataset.get_entity(entity_id)&.name || entity_id
95
- Sanitizer.table_name(entity_name, preserve_table_case?)
95
+ Sanitizer.table_name(entity_name)
96
96
  end
97
97
 
98
98
  # Build relationship definition
@@ -48,22 +48,12 @@ module Dbwatcher
48
48
  # Sanitize table name for Mermaid ERD
49
49
  #
50
50
  # @param name [String] raw table name
51
- # @param preserve_case [Boolean] whether to preserve original case
52
- # @return [String] sanitized table name
53
- def table_name(name, preserve_case = nil)
51
+ # @return [String] sanitized table name (preserves original case)
52
+ def table_name(name)
54
53
  return "UNKNOWN_TABLE" unless name.is_a?(String) && !name.empty?
55
54
 
56
- preserve = if preserve_case.nil?
57
- Dbwatcher.configuration.diagram_preserve_table_case
58
- else
59
- preserve_case
60
- end
61
-
62
- if preserve
63
- name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
64
- else
65
- name.to_s.upcase.gsub(/[^A-Z0-9_]/, "_")
66
- end
55
+ # Always preserve original case
56
+ name.to_s.gsub(/[^a-zA-Z0-9_]/, "_")
67
57
  end
68
58
 
69
59
  # Sanitize method name for Mermaid class diagrams
@@ -136,7 +136,7 @@ module Dbwatcher
136
136
  #
137
137
  # @return [Hash] loaded gems with versions
138
138
  def collect_loaded_gems
139
- return {} unless Dbwatcher.configuration.system_info_include_performance_metrics
139
+ return {} unless Dbwatcher.configuration.system_info_include_performance_metrics?
140
140
 
141
141
  gems = {}
142
142
  Gem.loaded_specs.each do |name, spec|
@@ -154,7 +154,7 @@ module Dbwatcher
154
154
  def collect_load_path_info
155
155
  {
156
156
  size: $LOAD_PATH.size,
157
- paths: Dbwatcher.configuration.system_info_include_performance_metrics ? $LOAD_PATH.first(10) : []
157
+ paths: Dbwatcher.configuration.system_info_include_performance_metrics? ? $LOAD_PATH.first(10) : []
158
158
  }
159
159
  rescue StandardError => e
160
160
  log_error "Failed to get load path info: #{e.message}"
@@ -166,7 +166,7 @@ module Dbwatcher
166
166
  # @return [Hash] filtered environment variables
167
167
  # rubocop:disable Metrics/MethodLength
168
168
  def collect_environment_variables
169
- return {} unless Dbwatcher.configuration.collect_sensitive_env_vars
169
+ return {} unless Dbwatcher.configuration.collect_sensitive_env_vars?
170
170
 
171
171
  env_vars = {}
172
172
 
@@ -280,14 +280,14 @@ module Dbwatcher
280
280
  300
281
281
  end,
282
282
  collect_sensitive_env_vars:
283
- if config.respond_to?(:collect_sensitive_env_vars)
284
- config.collect_sensitive_env_vars
283
+ if config.respond_to?(:collect_sensitive_env_vars?)
284
+ config.collect_sensitive_env_vars?
285
285
  else
286
286
  false
287
287
  end,
288
288
  system_info_include_performance_metrics:
289
- if config.respond_to?(:system_info_include_performance_metrics)
290
- config.system_info_include_performance_metrics
289
+ if config.respond_to?(:system_info_include_performance_metrics?)
290
+ config.system_info_include_performance_metrics?
291
291
  else
292
292
  true
293
293
  end
@@ -77,7 +77,7 @@ module Dbwatcher
77
77
  #
78
78
  # @return [Hash] machine information or empty hash on error
79
79
  def collect_machine_info
80
- return {} unless Dbwatcher.configuration.collect_system_info
80
+ return {} unless Dbwatcher.configuration.system_info
81
81
 
82
82
  MachineInfoCollector.call
83
83
  rescue StandardError => e
@@ -89,7 +89,7 @@ module Dbwatcher
89
89
  #
90
90
  # @return [Hash] database information or empty hash on error
91
91
  def collect_database_info
92
- return {} unless Dbwatcher.configuration.collect_system_info
92
+ return {} unless Dbwatcher.configuration.system_info
93
93
 
94
94
  DatabaseInfoCollector.call
95
95
  rescue StandardError => e
@@ -101,7 +101,7 @@ module Dbwatcher
101
101
  #
102
102
  # @return [Hash] runtime information or empty hash on error
103
103
  def collect_runtime_info
104
- return {} unless Dbwatcher.configuration.collect_system_info
104
+ return {} unless Dbwatcher.configuration.system_info
105
105
 
106
106
  RuntimeInfoCollector.call
107
107
  rescue StandardError => e
@@ -66,7 +66,29 @@ module Dbwatcher
66
66
  # @param change [Hash] change data
67
67
  # @return [String, nil] record ID if available
68
68
  def extract_record_id(change)
69
- change[:record_id] || change[:id] || change.dig(:changes, :id)
69
+ change[:record_id] || change[:id] || extract_id_from_changes(change[:changes])
70
+ end
71
+
72
+ # Extract ID from changes data (hash or array format)
73
+ #
74
+ # @param changes [Hash, Array, nil] changes data
75
+ # @return [String, nil] extracted ID
76
+ def extract_id_from_changes(changes)
77
+ return nil unless changes
78
+
79
+ case changes
80
+ when Hash then changes[:id]
81
+ when Array then extract_id_from_array_changes(changes)
82
+ end
83
+ end
84
+
85
+ # Extract ID from array of column changes
86
+ #
87
+ # @param changes [Array] array of column changes
88
+ # @return [String, nil] extracted ID value
89
+ def extract_id_from_array_changes(changes)
90
+ id_change = changes.find { |c| c.is_a?(Hash) && c[:column] == "id" }
91
+ id_change&.dig(:new_value) || id_change&.dig(:old_value)
70
92
  end
71
93
 
72
94
  # Format changes for timeline display