dbwatcher 1.0.0 → 1.1.0

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +81 -210
  3. data/app/assets/config/dbwatcher_manifest.js +15 -0
  4. data/app/assets/javascripts/dbwatcher/alpine_registrations.js +39 -0
  5. data/app/assets/javascripts/dbwatcher/auto_init.js +23 -0
  6. data/app/assets/javascripts/dbwatcher/components/base.js +141 -0
  7. data/app/assets/javascripts/dbwatcher/components/changes_table_hybrid.js +1008 -0
  8. data/app/assets/javascripts/dbwatcher/components/diagrams.js +449 -0
  9. data/app/assets/javascripts/dbwatcher/components/summary.js +234 -0
  10. data/app/assets/javascripts/dbwatcher/core/alpine_store.js +138 -0
  11. data/app/assets/javascripts/dbwatcher/core/api_client.js +162 -0
  12. data/app/assets/javascripts/dbwatcher/core/component_loader.js +70 -0
  13. data/app/assets/javascripts/dbwatcher/core/component_registry.js +94 -0
  14. data/app/assets/javascripts/dbwatcher/dbwatcher.js +120 -0
  15. data/app/assets/javascripts/dbwatcher/services/mermaid.js +315 -0
  16. data/app/assets/javascripts/dbwatcher/services/mermaid_service.js +199 -0
  17. data/app/assets/javascripts/dbwatcher/vendor/date-fns-browser.js +99 -0
  18. data/app/assets/javascripts/dbwatcher/vendor/lodash.min.js +140 -0
  19. data/app/assets/javascripts/dbwatcher/vendor/tabulator.min.js +3 -0
  20. data/app/assets/stylesheets/dbwatcher/application.css +423 -0
  21. data/app/assets/stylesheets/dbwatcher/application.scss +15 -0
  22. data/app/assets/stylesheets/dbwatcher/components/_badges.scss +38 -0
  23. data/app/assets/stylesheets/dbwatcher/components/_compact_table.scss +162 -0
  24. data/app/assets/stylesheets/dbwatcher/components/_diagrams.scss +51 -0
  25. data/app/assets/stylesheets/dbwatcher/components/_forms.scss +27 -0
  26. data/app/assets/stylesheets/dbwatcher/components/_navigation.scss +55 -0
  27. data/app/assets/stylesheets/dbwatcher/core/_base.scss +34 -0
  28. data/app/assets/stylesheets/dbwatcher/core/_variables.scss +47 -0
  29. data/app/assets/stylesheets/dbwatcher/vendor/tabulator.min.css +2 -0
  30. data/app/controllers/dbwatcher/api/v1/sessions_controller.rb +64 -0
  31. data/app/controllers/dbwatcher/base_controller.rb +8 -2
  32. data/app/controllers/dbwatcher/dashboard_controller.rb +8 -0
  33. data/app/controllers/dbwatcher/sessions_controller.rb +25 -10
  34. data/app/helpers/dbwatcher/component_helper.rb +29 -0
  35. data/app/helpers/dbwatcher/diagram_helper.rb +110 -0
  36. data/app/helpers/dbwatcher/session_helper.rb +3 -2
  37. data/app/views/dbwatcher/sessions/_changes_tab.html.erb +265 -0
  38. data/app/views/dbwatcher/sessions/_diagrams_tab.html.erb +166 -0
  39. data/app/views/dbwatcher/sessions/_session_header.html.erb +11 -0
  40. data/app/views/dbwatcher/sessions/_summary_tab.html.erb +88 -0
  41. data/app/views/dbwatcher/sessions/_tab_navigation.html.erb +12 -0
  42. data/app/views/dbwatcher/sessions/changes.html.erb +21 -0
  43. data/app/views/dbwatcher/sessions/components/changes/_filters.html.erb +44 -0
  44. data/app/views/dbwatcher/sessions/components/changes/_table_list.html.erb +96 -0
  45. data/app/views/dbwatcher/sessions/diagrams.html.erb +21 -0
  46. data/app/views/dbwatcher/sessions/index.html.erb +14 -10
  47. data/app/views/dbwatcher/sessions/shared/_layout.html.erb +8 -0
  48. data/app/views/dbwatcher/sessions/shared/_navigation.html.erb +35 -0
  49. data/app/views/dbwatcher/sessions/shared/_session_header.html.erb +25 -0
  50. data/app/views/dbwatcher/sessions/show.html.erb +3 -346
  51. data/app/views/dbwatcher/sessions/summary.html.erb +21 -0
  52. data/app/views/layouts/dbwatcher/application.html.erb +125 -247
  53. data/bin/compile_scss +49 -0
  54. data/config/routes.rb +26 -0
  55. data/lib/dbwatcher/configuration.rb +102 -8
  56. data/lib/dbwatcher/engine.rb +17 -7
  57. data/lib/dbwatcher/services/analyzers/session_data_processor.rb +98 -0
  58. data/lib/dbwatcher/services/analyzers/table_summary_builder.rb +202 -0
  59. data/lib/dbwatcher/services/api/base_api_service.rb +100 -0
  60. data/lib/dbwatcher/services/api/changes_data_service.rb +112 -0
  61. data/lib/dbwatcher/services/api/diagram_data_service.rb +145 -0
  62. data/lib/dbwatcher/services/api/summary_data_service.rb +158 -0
  63. data/lib/dbwatcher/services/base_service.rb +64 -0
  64. data/lib/dbwatcher/services/diagram_analyzers/base_analyzer.rb +162 -0
  65. data/lib/dbwatcher/services/diagram_analyzers/foreign_key_analyzer.rb +354 -0
  66. data/lib/dbwatcher/services/diagram_analyzers/inferred_relationship_analyzer.rb +502 -0
  67. data/lib/dbwatcher/services/diagram_analyzers/model_association_analyzer.rb +564 -0
  68. data/lib/dbwatcher/services/diagram_data/attribute.rb +154 -0
  69. data/lib/dbwatcher/services/diagram_data/dataset.rb +278 -0
  70. data/lib/dbwatcher/services/diagram_data/entity.rb +180 -0
  71. data/lib/dbwatcher/services/diagram_data/relationship.rb +188 -0
  72. data/lib/dbwatcher/services/diagram_data/relationship_params.rb +55 -0
  73. data/lib/dbwatcher/services/diagram_data.rb +65 -0
  74. data/lib/dbwatcher/services/diagram_error_handler.rb +239 -0
  75. data/lib/dbwatcher/services/diagram_generator.rb +154 -0
  76. data/lib/dbwatcher/services/diagram_strategies/base_diagram_strategy.rb +149 -0
  77. data/lib/dbwatcher/services/diagram_strategies/class_diagram_strategy.rb +49 -0
  78. data/lib/dbwatcher/services/diagram_strategies/erd_diagram_strategy.rb +52 -0
  79. data/lib/dbwatcher/services/diagram_strategies/flowchart_diagram_strategy.rb +52 -0
  80. data/lib/dbwatcher/services/diagram_system.rb +69 -0
  81. data/lib/dbwatcher/services/diagram_type_registry.rb +164 -0
  82. data/lib/dbwatcher/services/mermaid_syntax/base_builder.rb +127 -0
  83. data/lib/dbwatcher/services/mermaid_syntax/cardinality_mapper.rb +90 -0
  84. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_builder.rb +136 -0
  85. data/lib/dbwatcher/services/mermaid_syntax/class_diagram_helper.rb +46 -0
  86. data/lib/dbwatcher/services/mermaid_syntax/erd_builder.rb +116 -0
  87. data/lib/dbwatcher/services/mermaid_syntax/flowchart_builder.rb +109 -0
  88. data/lib/dbwatcher/services/mermaid_syntax/sanitizer.rb +102 -0
  89. data/lib/dbwatcher/services/mermaid_syntax_builder.rb +155 -0
  90. data/lib/dbwatcher/storage/api/concerns/table_analyzer.rb +15 -128
  91. data/lib/dbwatcher/storage/api/session_api.rb +47 -0
  92. data/lib/dbwatcher/storage/base_storage.rb +7 -0
  93. data/lib/dbwatcher/version.rb +1 -1
  94. data/lib/dbwatcher.rb +58 -1
  95. metadata +94 -2
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ module Api
6
+ # Service for handling summary statistics data
7
+ #
8
+ # Provides enhanced summary data for the sessions summary view and API endpoints
9
+ # with caching and comprehensive statistics.
10
+ class SummaryDataService < BaseApiService
11
+ def call
12
+ start_time = Time.now
13
+ log_service_start("Getting summary data for session #{session.id}")
14
+
15
+ validation_error = validate_session
16
+ return validation_error if validation_error
17
+
18
+ begin
19
+ result = with_cache do
20
+ build_summary_response
21
+ end
22
+
23
+ log_service_completion(start_time, session_id: session.id)
24
+ result
25
+ rescue StandardError => e
26
+ handle_error(e)
27
+ end
28
+ end
29
+
30
+ private
31
+
32
+ def build_summary_response
33
+ summary_data = base_summary
34
+
35
+ return summary_data if summary_data[:error]
36
+
37
+ enhance_summary_data(summary_data)
38
+ end
39
+
40
+ def base_summary
41
+ result = Storage.sessions.summary(session.id)
42
+
43
+ if result[:error]
44
+ log_error "Base summary error: #{result[:error]}"
45
+ return { error: result[:error] }
46
+ end
47
+
48
+ result
49
+ end
50
+
51
+ def enhance_summary_data(base_summary)
52
+ tables_summary = Storage.sessions.build_tables_summary(session)
53
+
54
+ base_summary.merge(
55
+ session_id: session.id,
56
+ enhanced_stats: build_enhanced_stats(tables_summary),
57
+ tables_breakdown: build_tables_breakdown(tables_summary),
58
+ metadata: build_metadata,
59
+ timing: build_timing_info
60
+ )
61
+ end
62
+
63
+ def build_enhanced_stats(tables_summary)
64
+ {
65
+ tables_count: tables_summary.keys.length,
66
+ total_changes: calculate_total_changes(tables_summary),
67
+ operations_breakdown: calculate_operations_breakdown(tables_summary),
68
+ tables_with_changes: tables_summary.keys,
69
+ most_active_table: find_most_active_table(tables_summary),
70
+ change_distribution: calculate_change_distribution(tables_summary)
71
+ }
72
+ end
73
+
74
+ def build_tables_breakdown(tables_summary)
75
+ tables_data = tables_summary.map do |table_name, data|
76
+ {
77
+ table_name: table_name,
78
+ change_count: data[:changes]&.length || 0,
79
+ operations: data[:operations] || {},
80
+ sample_columns: data[:sample_record]&.keys || []
81
+ }
82
+ end
83
+ tables_data.sort_by { |table| -table[:change_count] }
84
+ end
85
+
86
+ def build_metadata
87
+ {
88
+ generated_at: Time.current,
89
+ cache_key: cache_key,
90
+ data_freshness: "cached"
91
+ }
92
+ end
93
+
94
+ def calculate_total_changes(tables_summary)
95
+ tables_summary.values.sum { |data| data[:changes]&.length || 0 }
96
+ end
97
+
98
+ def calculate_operations_breakdown(tables_summary)
99
+ operations = { "INSERT" => 0, "UPDATE" => 0, "DELETE" => 0 }
100
+
101
+ tables_summary.each_value do |data|
102
+ data[:operations]&.each do |op, count|
103
+ normalized_op = op.to_s.upcase
104
+ operations[normalized_op] += count if operations.key?(normalized_op)
105
+ end
106
+ end
107
+
108
+ operations
109
+ end
110
+
111
+ def find_most_active_table(tables_summary)
112
+ return nil if tables_summary.empty?
113
+
114
+ most_active = tables_summary.max_by do |_, data|
115
+ data[:changes]&.length || 0
116
+ end
117
+
118
+ {
119
+ table_name: most_active[0],
120
+ change_count: most_active[1][:changes]&.length || 0
121
+ }
122
+ end
123
+
124
+ def calculate_change_distribution(tables_summary)
125
+ total_changes = calculate_total_changes(tables_summary)
126
+ return {} if total_changes.zero?
127
+
128
+ distribution = {}
129
+ tables_summary.each do |table_name, data|
130
+ change_count = data[:changes]&.length || 0
131
+ percentage = (change_count.to_f / total_changes * 100).round(2)
132
+ distribution[table_name] = {
133
+ count: change_count,
134
+ percentage: percentage
135
+ }
136
+ end
137
+
138
+ distribution
139
+ end
140
+
141
+ def build_timing_info
142
+ {
143
+ started_at: session.started_at,
144
+ ended_at: session.ended_at,
145
+ duration: calculate_duration
146
+ }
147
+ end
148
+
149
+ def calculate_duration
150
+ return nil unless session.started_at
151
+
152
+ end_time = session.ended_at || Time.current
153
+ ((end_time.to_time - session.started_at.to_time) * 1000).round
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dbwatcher
4
+ module Services
5
+ # Base class for all service objects
6
+ #
7
+ # Provides common functionality and patterns for service objects
8
+ # including logging and consistent call interface.
9
+ #
10
+ # @example
11
+ # class MyService < BaseService
12
+ # def call
13
+ # log_info "Starting service"
14
+ # # service logic
15
+ # end
16
+ # end
17
+ #
18
+ # result = MyService.call
19
+ class BaseService
20
+ include Dbwatcher::Logging
21
+
22
+ # Class method to create instance and call
23
+ #
24
+ # @param args [Array] arguments to pass to initialize
25
+ # @return [Object] result of call method
26
+ def self.call(*args)
27
+ new(*args).call
28
+ end
29
+
30
+ # Initialize service
31
+ #
32
+ # @param args [Array] service arguments
33
+ def initialize(*args)
34
+ # Override in subclasses
35
+ end
36
+
37
+ # Perform service operation
38
+ #
39
+ # @return [Object] service result
40
+ def call
41
+ raise NotImplementedError, "#{self.class} must implement #call"
42
+ end
43
+
44
+ private
45
+
46
+ # Log service start with context
47
+ #
48
+ # @param message [String] log message
49
+ # @param context [Hash] additional context
50
+ def log_service_start(message, context = {})
51
+ log_info "#{self.class.name}: #{message}", context
52
+ end
53
+
54
+ # Log service completion with duration
55
+ #
56
+ # @param start_time [Time] service start time
57
+ # @param context [Hash] additional context
58
+ def log_service_completion(start_time, context = {})
59
+ duration = Time.now - start_time
60
+ log_info "#{self.class.name}: Completed in #{duration.round(3)}s", context.merge(duration: duration)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../diagram_data/relationship_params"
4
+
5
+ module Dbwatcher
6
+ module Services
7
+ module DiagramAnalyzers
8
+ # Abstract base class for diagram analyzers
9
+ #
10
+ # This class provides a standard interface that all diagram analyzers must implement
11
+ # to ensure consistent data flow and transformation to Dataset format.
12
+ #
13
+ # @example
14
+ # class CustomAnalyzer < BaseAnalyzer
15
+ # def analyze(context)
16
+ # # Perform analysis and return raw data
17
+ # end
18
+ #
19
+ # def transform_to_dataset(raw_data)
20
+ # # Transform raw data to Dataset
21
+ # end
22
+ #
23
+ # def analyzer_type
24
+ # "custom"
25
+ # end
26
+ # end
27
+ class BaseAnalyzer < BaseService
28
+ # Standard interface that all analyzers must implement
29
+ #
30
+ # @param context [Hash] analysis context (session, options, etc.)
31
+ # @return [Object] raw analysis data in analyzer-specific format
32
+ # @raise [NotImplementedError] if not implemented by subclass
33
+ def analyze(context)
34
+ raise NotImplementedError, "Subclasses must implement analyze method"
35
+ end
36
+
37
+ # Transform raw data to standard Dataset
38
+ #
39
+ # @param raw_data [Object] raw data from analyze method
40
+ # @return [DiagramData::Dataset] standardized dataset
41
+ # @raise [NotImplementedError] if not implemented by subclass
42
+ def transform_to_dataset(raw_data)
43
+ raise NotImplementedError, "Subclasses must implement transform_to_dataset"
44
+ end
45
+
46
+ # Get analyzer type classification
47
+ #
48
+ # @return [String] analyzer type identifier
49
+ # @raise [NotImplementedError] if not implemented by subclass
50
+ def analyzer_type
51
+ raise NotImplementedError, "Subclasses must implement analyzer_type"
52
+ end
53
+
54
+ # Main entry point - analyze and transform
55
+ #
56
+ # @return [DiagramData::Dataset] standardized dataset
57
+ def call
58
+ log_service_start "Starting #{self.class.name}", analysis_context
59
+ start_time = Time.current
60
+
61
+ begin
62
+ # Perform analysis
63
+ raw_data = analyze(analysis_context)
64
+
65
+ # Transform to standard format
66
+ dataset = transform_to_dataset(raw_data)
67
+
68
+ # Validate result
69
+ unless dataset.is_a?(Dbwatcher::Services::DiagramData::Dataset)
70
+ raise StandardError, "transform_to_dataset must return a Dataset instance"
71
+ end
72
+
73
+ unless dataset.valid?
74
+ Rails.logger.warn "#{self.class.name}: Generated invalid dataset: #{dataset.validation_errors.join(", ")}"
75
+ end
76
+
77
+ log_service_completion(start_time, {
78
+ entities_count: dataset.entities.size,
79
+ relationships_count: dataset.relationships.size,
80
+ dataset_valid: dataset.valid?
81
+ })
82
+
83
+ dataset
84
+ rescue StandardError => e
85
+ Rails.logger.error "#{self.class.name} error: #{e.class}: #{e.message}\n#{e.backtrace.join("\n")}"
86
+ # Return empty dataset instead of failing
87
+ create_empty_dataset
88
+ end
89
+ end
90
+
91
+ protected
92
+
93
+ # Build analysis context for this analyzer
94
+ #
95
+ # @return [Hash] analysis context
96
+ def analysis_context
97
+ # Override in subclasses to provide specific context
98
+ {}
99
+ end
100
+
101
+ # Create empty dataset with metadata
102
+ #
103
+ # @return [DiagramData::Dataset] empty dataset
104
+ def create_empty_dataset
105
+ Dbwatcher::Services::DiagramData::Dataset.new(
106
+ metadata: {
107
+ analyzer: self.class.name,
108
+ analyzer_type: analyzer_type,
109
+ empty_reason: "No data found or analysis failed",
110
+ generated_at: Time.current.iso8601
111
+ }
112
+ )
113
+ end
114
+
115
+ # Helper method to create entities
116
+ #
117
+ # @param id [String] entity ID
118
+ # @param name [String] entity name
119
+ # @param type [String] entity type
120
+ # @param attributes [Array<Attribute>] entity attributes/properties
121
+ # @param metadata [Hash] entity metadata
122
+ # @return [DiagramData::Entity] new entity
123
+ def create_entity(id:, name:, type: "default", attributes: [], metadata: {})
124
+ Dbwatcher::Services::DiagramData::Entity.new(
125
+ id: id,
126
+ name: name,
127
+ type: type,
128
+ attributes: attributes,
129
+ metadata: metadata
130
+ )
131
+ end
132
+
133
+ # Helper method to create relationships
134
+ #
135
+ # @param params [Hash] relationship parameters
136
+ # @return [DiagramData::Relationship] new relationship
137
+ def create_relationship(params)
138
+ params_obj = Dbwatcher::Services::DiagramData::RelationshipParams.new(params)
139
+ Dbwatcher::Services::DiagramData::Relationship.new(params_obj)
140
+ end
141
+
142
+ # Helper method to create attributes
143
+ #
144
+ # @param name [String] attribute name
145
+ # @param type [String] attribute data type
146
+ # @param nullable [Boolean] whether attribute can be null
147
+ # @param default [Object] default value
148
+ # @param metadata [Hash] additional type-specific information
149
+ # @return [DiagramData::Attribute] new attribute
150
+ def create_attribute(name:, type: nil, nullable: true, default: nil, metadata: {})
151
+ Dbwatcher::Services::DiagramData::Attribute.new(
152
+ name: name,
153
+ type: type,
154
+ nullable: nullable,
155
+ default: default,
156
+ metadata: metadata
157
+ )
158
+ end
159
+ end
160
+ end
161
+ end
162
+ end